diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3437a3213..5d99e7506 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* `shiny create` now supports a succinct format for specifying the GitHub repository via the `--github` flag, e.g. `--github posit-dev/py-shiny-templates`. You can now also use `--github` and `--template` together, in which case `--github` should point to a repository containing a directory matching the name provided in `--template`. (#1623)
+* `shiny create` now identifies templates in external repositories using a `_template.json` metadata file. This file should contain at an `"id"` and optionally a `"title"` and `"description"`. When `shiny create` is called with the `--github` flag but without a `--template` flag, it will offer a menu listing all available templates in the repository. (#1631)
+
### Other changes
### Bug fixes
diff --git a/shiny/_main.py b/shiny/_main.py
index 4d2b98cd9..e004f4312 100644
--- a/shiny/_main.py
+++ b/shiny/_main.py
@@ -582,17 +582,21 @@ def create(
dir: Optional[Path | str] = None,
package_name: Optional[str] = None,
) -> None:
- from ._main_create import use_template_github, use_template_internal
-
- print(f"dir is {dir}")
+ from ._main_create import use_github_template, use_internal_template
if dir is not None:
dir = Path(dir)
if github is not None:
- use_template_github(github, template=template, mode=mode, dest_dir=dir)
+ use_github_template(
+ github,
+ template_name=template,
+ mode=mode,
+ dest_dir=dir,
+ package_name=package_name,
+ )
else:
- use_template_internal(template, mode, dir, package_name)
+ use_internal_template(template, mode, dir, package_name)
@main.command(
diff --git a/shiny/_main_create.py b/shiny/_main_create.py
index bf19157ba..0230fac4b 100644
--- a/shiny/_main_create.py
+++ b/shiny/_main_create.py
@@ -1,5 +1,6 @@
from __future__ import annotations
+import json
import os
import re
import shutil
@@ -7,9 +8,9 @@
import tempfile
import textwrap
import zipfile
-from dataclasses import dataclass
+from dataclasses import dataclass, field
from pathlib import Path
-from typing import Generator, Optional, cast
+from typing import Generator, Literal, Optional, cast
from urllib.error import URLError
from urllib.parse import urlparse
from urllib.request import urlopen
@@ -34,43 +35,207 @@
cli_url,
cli_verbatim,
cli_wait,
+ cli_warning,
directory_prompt,
)
-# These templates are copied over from the `shiny/templates/app_templates`
-# directory. The process for adding new ones is to add your app folder to
-# that directory, and then add another entry to this dictionary.
-app_template_choices = {
- "Basic app": "basic-app",
- "Sidebar layout": "basic-sidebar",
- "Basic dashboard": "dashboard",
- "Intermediate dashboard": "dashboard-tips",
- "Navigating multiple pages/panels": "basic-navigation",
- "Custom JavaScript component ...": "js-component",
- "Choose from the Shiny Templates website": "external-gallery",
-}
-
-# These are templates which produce a Python package and have content filled in at
-# various places based on the user input. You can add new ones by following the
-# examples in `shiny/templates/package-templates` and then adding entries to this
-# dictionary.
-package_template_choices = {
- "Input component": "js-input",
- "Output component": "js-output",
- "React component": "js-react",
-}
-
styles_for_questions = questionary.Style([("secondary", "italic")])
# Prebuild some common choices
cancel_choice: Choice = Choice(title=[("class:secondary", "[Cancel]")], value="cancel")
back_choice: Choice = Choice(title=[("class:secondary", "← Back")], value="back")
-def choice_from_dict(choice_dict: dict[str, str]) -> list[Choice]:
- return [Choice(title=key, value=value) for key, value in choice_dict.items()]
+def choice_from_templates(templates: list[ShinyTemplate]) -> list[Choice]:
+ return [Choice(title=t.title, value=t.id) for t in templates]
+
+
+@dataclass
+class ShinyTemplate:
+ """
+ Shiny Template
+
+ This class receives metadata for a Shiny template from a `_template.json` file.
+ (Alternatively, a template can be created from just `name` and `path` for legacy
+ reasons.)
+
+ Attributes
+ ----------
+ id
+ The identifier of the Shiny template. This `id` should be unique within a
+ repository of templates.
+ path
+ The path to the `_template.json` file or the root directory of the template.
+ type
+ The type of the template (e.g. "app", "package"), default: 'app'.
+ title
+ A brief title for the template, if provided.
+ description
+ A longer description of the template, if provided.
+ next_steps
+ A list of next steps or instructions related to this template, shown after the
+ default instructions are displayed. In the `_template.json` file, this field
+ can be a single string or an array of strings.
+ follow_up
+ A list of follow-up actions or information related to this template. In the
+ `_template.json` file, this field can be a single string, an array of strings,
+ or an object with a `text` field and an optional `type` field. The `type` field
+ can be "action", "info", "warning", "danger", or "text".
+ """
+
+ id: str
+ path: Path
+ type: str = "app"
+ title: str | None = None
+ description: str | None = None
+ next_steps: list[str] = field(default_factory=list)
+ follow_up: list[ShinyTemplateFollowUp] = field(default_factory=list)
+ _express_available: bool | None = None
+
+ @property
+ def express_available(self) -> bool:
+ """
+ Does the template include an Express variant, denoted by the presence of an
+ `app-express.py` file?
+ """
+
+ if self._express_available is None:
+ self._express_available = (self.path / "app-express.py").exists()
+ return self._express_available
+
+
+class ShinyTemplateFollowUp:
+ def __init__(
+ self,
+ text: str,
+ type: str = "text",
+ ):
+ self.text = text
+ self.type: Literal["action", "info", "warning", "danger", "text"] = "text"
+ if type in ("action", "info", "warning", "danger"):
+ self.type = type
+
+
+def find_templates(path: Path | str = ".") -> list[ShinyTemplate]:
+ path = Path(path)
+ templates: list[ShinyTemplate] = []
+ duplicated_ids: set[str] = set()
+
+ template_files = sorted(path.glob("**/_template.json"))
+ for tf in template_files:
+ with tf.open() as f:
+ try:
+ template = json.load(f)
+ except json.JSONDecodeError as err:
+ raise ValueError(f"Error parsing {tf}: {err}")
+
+ # "next_steps" and "follow_up" can be either a string or an array of strings
+ # or an array of dictionaries (follow_up only)
+ follow_up_raw: str | list[dict[str, str]] = template.get("follow_up", [])
+ if isinstance(follow_up_raw, str):
+ follow_up_raw = [{"text": follow_up_raw}]
+
+ follow_up = [ShinyTemplateFollowUp(**f) for f in follow_up_raw]
+
+ next_steps: str | list[str] = template.get("next_steps", [])
+ if isinstance(next_steps, str):
+ next_steps = [next_steps]
+
+ if "id" not in template:
+ raise ValueError(f"Template in {tf} is missing a 'id' field.")
+
+ id = template["id"]
+ if id in [t.id for t in templates]:
+ duplicated_ids.add(id)
+
+ templates.append(
+ ShinyTemplate(
+ id=id,
+ path=tf.parent.absolute(),
+ title=template.get("title"),
+ type=template.get("type", "app"),
+ description=template.get("description"),
+ follow_up=follow_up,
+ next_steps=next_steps,
+ )
+ )
+
+ if duplicated_ids:
+ click.echo(
+ cli_danger(
+ "Warning: The following templates contain duplicate IDs. "
+ + "Only the first occurrence will be used."
+ )
+ )
+ for id in duplicated_ids:
+ paths = [t.path.relative_to(path) for t in templates if t.id == id]
+ click.echo(
+ cli_warning(
+ cli_code(f'"id": "{id}"')
+ + " used by: "
+ + ", ".join([cli_field(str(p)) for p in paths])
+ )
+ )
+
+ return templates
+
+
+def template_by_name(templates: list[ShinyTemplate], name: str) -> ShinyTemplate | None:
+ for template in templates:
+ if template.id == name:
+ return template
+ return None
+
+
+class ShinyInternalTemplates:
+ """
+ Shiny's Internal Templates
+
+ Internal templates that are built into the shiny package are always available via
+ `shiny create`. These templates are stored in the `shiny/templates` directory and
+ are divided into `app-templates` and `package-templates`.
+
+ To add a new template, create the template subfolder in either of the two template
+ folders and add a `_template.json` file. See `ShinyTemplate` for expected fields.
+
+ * `use_template_internal()` is the initial menu seen, which presents `templates/app`
+ templates with additional choices.
+ * `templates/package` templates are also referred to as `js-components` in the code
+ base, these templates appear as a submenu and are handled by
+ `use_internal_package_template()`.
+ * `templates/chat` templates are generative AI templates and are handled by
+ `use_internal_chat_ai_template()`,
+ """
+
+ def __init__(self):
+ self.templates: dict[str, list[ShinyTemplate]] = {}
+
+ def _templates(self, dir: str = "templates") -> list[ShinyTemplate]:
+ if dir in self.templates:
+ return self.templates[dir]
+ self.templates[dir] = find_templates(Path(__file__).parent / dir)
+ return self.templates[dir]
+ @property
+ def apps(self) -> list[ShinyTemplate]:
+ return self._templates("templates/app")
-def use_template_internal(
+ @property
+ def packages(self) -> list[ShinyTemplate]:
+ return self._templates("templates/package")
+
+ @property
+ def chat_hello_providers(self) -> list[ShinyTemplate]:
+ return self._templates("templates/chat/hello-providers")
+
+ @property
+ def chat_enterprise(self) -> list[ShinyTemplate]:
+ return self._templates("templates/chat/enterprise")
+
+
+shiny_internal_templates = ShinyInternalTemplates()
+
+
+def use_internal_template(
question_state: Optional[str] = None,
mode: Optional[str] = None,
dest_dir: Optional[Path] = None,
@@ -85,35 +250,44 @@ def use_template_internal(
were at level 5 of a question chain and wanted to return to level 4.
This is not that useful currently because we only have two levels of questions.
-
:param question_state: The question state you would like to return to. Currently, the options are:
"cancel": Cancel the operation and exit.
"js-component": Start the questions for creating a custom JavaScript component.
"""
+ app_templates = shiny_internal_templates.apps
+ pkg_templates = shiny_internal_templates.packages
+ chat_templates = [
+ *shiny_internal_templates.chat_hello_providers,
+ *shiny_internal_templates.chat_enterprise,
+ ]
+
+ menu_choices = [
+ Choice(title="Custom JavaScript component...", value="_js-component"),
+ Choice(title="Generative AI templates...", value="_chat-ai"),
+ Choice(
+ title="Choose from the Shiny Templates website", value="_external-gallery"
+ ),
+ ]
+
if question_state is None:
- template = questionary.select(
- "Which template would you like to use?:",
- choices=[*choice_from_dict(app_template_choices), cancel_choice],
- style=styles_for_questions,
- ).ask()
- else:
- template = question_state
-
- valid_template_choices = {**app_template_choices, **package_template_choices}
- if template not in valid_template_choices.values():
- raise click.BadOptionUsage(
- "--template",
- f"Invalid value for '--template' / '-t': {template} is not one of "
- + f"""'{"', '".join(valid_template_choices.values())}'.""",
- )
+ question_state = question_choose_template(app_templates, *menu_choices)
- # Define the control flow for the top level menu
- if template is None or template == "cancel":
- sys.exit(1)
- elif template == "external-gallery":
- url = cli_url("https://shiny.posit.co/py/templates")
- click.echo(f"Opening {url} in your browser.")
+ template = template_by_name(
+ [*app_templates, *pkg_templates, *chat_templates], question_state
+ )
+
+ if template is not None:
+ if template.type == "app":
+ return app_template_questions(template, mode, dest_dir=dest_dir)
+ if template.type == "package":
+ return package_template_questions(
+ template, dest_dir=dest_dir, package_name=package_name
+ )
+
+ if question_state == "_external-gallery":
+ url = "https://shiny.posit.co/py/templates"
+ click.echo(f"Opening {cli_url(url)} in your browser.")
click.echo(
f"Choose a template and copy the {cli_code('shiny create')} command to use it."
)
@@ -121,13 +295,124 @@ def use_template_internal(
webbrowser.open(url)
sys.exit(0)
- elif template == "js-component":
- js_component_questions(dest_dir=dest_dir, package_name=package_name)
- return
- elif template in package_template_choices.values():
- js_component_questions(template, dest_dir=dest_dir, package_name=package_name)
+ elif question_state == "_js-component":
+ use_internal_package_template(dest_dir=dest_dir, package_name=package_name)
+ elif question_state == "_chat-ai":
+ use_internal_chat_ai_template(dest_dir=dest_dir, package_name=package_name)
else:
- app_template_questions(template, mode, dest_dir=dest_dir)
+ valid_choices = [t.id for t in app_templates + pkg_templates]
+ if question_state not in valid_choices:
+ raise click.BadOptionUsage(
+ "--template",
+ f"Invalid value for '--template' / '-t': {question_state} is not one of "
+ + f"""'{"', '".join(valid_choices)}'.""",
+ )
+
+
+def use_internal_package_template(
+ dest_dir: Optional[Path] = None,
+ package_name: Optional[str] = None,
+):
+ input = questionary.select(
+ "What kind of component do you want to build?",
+ choices=[
+ *choice_from_templates(shiny_internal_templates.packages),
+ back_choice,
+ cancel_choice,
+ ],
+ style=styles_for_questions,
+ ).ask()
+
+ if input == "back":
+ use_internal_template()
+ return
+
+ if input is None or input == "cancel":
+ sys.exit(1)
+
+ template = template_by_name(shiny_internal_templates.packages, input)
+
+ if template is None:
+ # This should be valid because we're selecting from the list of templates
+ # but just in case and to make type checkers happy
+ raise ValueError(f"Package template for {input} not found.")
+
+ package_template_questions(template, dest_dir=dest_dir, package_name=package_name)
+
+
+def use_internal_chat_ai_template(
+ input: str | None = None,
+ dest_dir: Optional[Path] = None,
+ package_name: Optional[str] = None,
+):
+ if input is None:
+ input = questionary.select(
+ "Which kind of generative AI template would you like to use?",
+ choices=[
+ Choice(title="By provider...", value="_chat-ai_hello-providers"),
+ Choice(title="Enterprise providers...", value="_chat-ai_enterprise"),
+ back_choice,
+ cancel_choice,
+ ],
+ style=styles_for_questions,
+ ).ask()
+
+ if input is None or input == "cancel":
+ sys.exit(1)
+
+ if input == "back":
+ use_internal_template(dest_dir=dest_dir, package_name=package_name)
+ return
+
+ use_internal_chat_ai_template(
+ input, dest_dir=dest_dir, package_name=package_name
+ )
+ return
+
+ template_choices = (
+ shiny_internal_templates.chat_enterprise
+ if input == "_chat-ai_enterprise"
+ else shiny_internal_templates.chat_hello_providers
+ )
+
+ choice = question_choose_template(template_choices, back_choice)
+
+ if choice == "back":
+ use_internal_chat_ai_template(dest_dir=dest_dir, package_name=package_name)
+ return
+
+ template = template_by_name(
+ [
+ *shiny_internal_templates.chat_hello_providers,
+ *shiny_internal_templates.chat_enterprise,
+ ],
+ choice,
+ )
+
+ if template is None:
+ raise ValueError(f"Chat AI template for {choice} not found.")
+
+ app_template_questions(template, dest_dir=dest_dir, mode=None)
+
+
+def question_choose_template(
+ templates: list[ShinyTemplate],
+ *extras: Choice,
+) -> str:
+ """
+ Ask the user to pick one of the templates. Includes and handles the cancel choice.
+ """
+
+ choice = questionary.select(
+ "Which template would you like to use?",
+ choices=[*choice_from_templates(templates), *extras, cancel_choice],
+ style=styles_for_questions,
+ ).ask()
+
+ if choice is None or choice == "cancel":
+ sys.exit(1)
+
+ return choice
def download_and_extract_zip(url: str, temp_dir: Path) -> Path:
@@ -159,11 +444,12 @@ def download_and_extract_zip(url: str, temp_dir: Path) -> Path:
return temp_dir
-def use_template_github(
+def use_github_template(
github: str,
- template: str | None = None,
+ template_name: str | None = None,
mode: str | None = None,
dest_dir: Path | None = None,
+ package_name: str | None = None,
):
# Github requires that we download the whole repository, so we need to
# download and unzip the repo, then navigate to the subdirectory.
@@ -208,12 +494,53 @@ def use_template_github(
f"Template directory '{cli_input(spec.path)}' does not exist in {cli_field(spec_cli)}."
)
- return app_template_questions(
- template=template,
- mode=mode,
- template_dir=Path(template_dir),
- dest_dir=dest_dir,
- )
+ templates = find_templates(template_dir)
+
+ if not templates:
+ # Legacy: repo doesn't have _template.json files, so we have to rely on
+ # paths, i.e. template_dir / template_name
+ if template_name is None:
+ # warn that we're assuming the repo spec points to the template directly
+ click.echo(
+ cli_info(
+ f"Using {cli_field(spec_cli)} as the template. "
+ + f"Use {cli_code('--template')} to specify a template otherwise."
+ )
+ )
+ template_name = template_dir.name
+ else:
+ template_dir = template_dir / template_name
+
+ template = ShinyTemplate(
+ id=template_name,
+ title=f"Template from {spec_cli}",
+ path=template_dir,
+ )
+ elif template_name:
+ # Repo has templates and the user already picked one
+ template = template_by_name(templates, template_name)
+ if not template:
+ raise click.ClickException(
+ f"Template '{cli_input(template_name)}' not found in {cli_field(spec_cli)}."
+ )
+ else:
+ # Has templates, but the user needs to pick one
+ template_name = question_choose_template(templates)
+ template = template_by_name(templates, template_name)
+
+ if not template:
+ raise click.ClickException(
+ f"Template '{cli_input(template_name)}' not found in {cli_field(spec_cli)}."
+ )
+
+ if template.type == "package":
+ return package_template_questions(
+ template,
+ dest_dir=dest_dir,
+ package_name=package_name,
+ )
+ else:
+ return app_template_questions(template, dest_dir=dest_dir, mode=mode)
def github_zip_url(spec: GithubRepoLocation) -> Generator[str]:
@@ -318,41 +645,22 @@ def parse_github_url(x: str) -> GithubRepoLocation:
def app_template_questions(
- template: Optional[str] = None,
+ template: ShinyTemplate,
mode: Optional[str] = None,
- template_dir: Optional[Path] = None,
dest_dir: Optional[Path] = None,
):
- if template_dir is None:
- if template is None:
- raise ValueError("You must provide either template or template_dir")
- template_dir = Path(__file__).parent / "templates/app-templates" / template
- elif template is not None:
- template_dir = template_dir / template
-
- # FIXME: We don't have any special syntax of files to signal a "template", which
- # means that we could end up here with `template_dir` being a repo of templates. If
- # `template` is missing, we end up copying everything in `template_dir` as if it's
- # all part of a single big template. When we introduce a way to signal or coordinate
- # templates in a repo, we will add a check here to avoid copying more than one
- # template.
- click.echo(
- cli_wait(
- f"Creating Shiny app from template {cli_bold(cli_field(template_dir.name))}..."
- )
- )
+ template_dir = template.path
+ template_cli_name = cli_bold(cli_field(template.title or template.id))
- # Not all apps will be implemented in both express and core so we can
- # avoid the questions if it's a core only app.
- template_files = [file.name for file in template_dir.iterdir() if file.is_file()]
- express_available = "app-express.py" in template_files
-
- if mode == "express" and not express_available:
- raise Exception("Express mode not available for that template.")
+ if mode == "express" and not template.express_available:
+ raise click.BadParameter(
+ f"Express mode not available for the {template_cli_name} template."
+ )
+ click.echo(cli_wait(f"Creating {template_cli_name} Shiny app..."))
dest_dir = directory_prompt(dest_dir, template_dir.name)
- if mode is None and express_available:
+ if mode is None and template.express_available:
mode = questionary.select(
"Would you like to use Shiny Express?",
[
@@ -366,15 +674,10 @@ def app_template_questions(
if mode is None or mode == "cancel":
sys.exit(1)
if mode == "back":
- use_template_internal()
+ use_internal_template()
return
- app_dir = copy_template_files(
- dest_dir,
- template_dir=template_dir,
- express_available=express_available,
- mode=mode,
- )
+ app_dir = copy_template_files(template, dest_dir, mode=mode)
click.echo(cli_success(f"Created Shiny app at {cli_field(str(app_dir))}"))
click.echo()
@@ -392,36 +695,14 @@ def app_template_questions(
)
click.echo(f"- Open and edit the app file: {cli_field(str(app_dir / 'app.py'))}")
+ click_echo_next_steps_and_follow_up(template)
+
-def js_component_questions(
- component_type: Optional[str] = None,
+def package_template_questions(
+ template: ShinyTemplate,
dest_dir: Optional[Path] = None,
package_name: Optional[str] = None,
):
- """
- Hand question branch for the custom js templates. This should handle the entire rest
- of the question flow and is responsible for placing files etc. Currently it repeats
- a lot of logic from the default flow but as the custom templates get more
- complicated the logic will diverge
- """
- if component_type is None:
- component_type = questionary.select(
- "What kind of component do you want to build?:",
- choices=[
- *choice_from_dict(package_template_choices),
- back_choice,
- cancel_choice,
- ],
- style=styles_for_questions,
- ).ask()
-
- if component_type == "back":
- use_template_internal()
- return
-
- if component_type is None or component_type == "cancel":
- sys.exit(1)
-
# Ask what the user wants the name of their component to be
if package_name is None:
package_name = questionary.text(
@@ -433,20 +714,12 @@ def js_component_questions(
if package_name is None:
sys.exit(1)
- template_dir = (
- Path(__file__).parent / "templates/package-templates" / component_type
- )
-
- dest_dir = directory_prompt(dest_dir, package_name)
-
app_dir = copy_template_files(
- dest_dir,
- template_dir=template_dir,
- express_available=False,
+ template,
+ dest_dir=directory_prompt(dest_dir, package_name),
mode=None,
)
- # Print messsage saying we're building the component
click.echo(cli_wait(f"Setting up {cli_field(package_name)} component package..."))
update_component_name_in_template(app_dir, package_name)
@@ -468,21 +741,22 @@ def js_component_questions(
f"- Open and run the example app in the {cli_field('example-app')} directory"
)
+ click_echo_next_steps_and_follow_up(template)
+
def copy_template_files(
- app_dir: Path,
- template_dir: Path,
- express_available: bool,
+ template: ShinyTemplate,
+ dest_dir: Path,
mode: Optional[str] = None,
):
- files_to_check = [file.name for file in template_dir.iterdir()]
+ files_to_check = [file.name for file in template.path.iterdir()]
if "__pycache__" in files_to_check:
files_to_check.remove("__pycache__")
files_to_check.append("app.py")
- duplicate_files = [file for file in files_to_check if (app_dir / file).exists()]
+ duplicate_files = [file for file in files_to_check if (dest_dir / file).exists()]
if any(duplicate_files):
err_files = ", ".join([cli_input('"' + file + '"') for file in duplicate_files])
@@ -494,26 +768,53 @@ def copy_template_files(
)
sys.exit(1)
- if not app_dir.exists():
- app_dir.mkdir()
+ if not dest_dir.exists():
+ dest_dir.mkdir()
- for item in template_dir.iterdir():
+ for item in template.path.iterdir():
if item.is_file():
- shutil.copy(item, app_dir / item.name)
+ if item.name == "_template.json":
+ continue
+ shutil.copy(item, dest_dir / item.name)
else:
if item.name != "__pycache__":
- shutil.copytree(item, app_dir / item.name)
+ shutil.copytree(item, dest_dir / item.name)
- def rename_unlink(file_to_rename: str, file_to_delete: str, dir: Path = app_dir):
+ def rename_unlink(file_to_rename: str, file_to_delete: str, dir: Path = dest_dir):
(dir / file_to_rename).rename(dir / "app.py")
(dir / file_to_delete).unlink()
- if express_available:
+ if template.express_available:
if mode == "express":
rename_unlink("app-express.py", "app-core.py")
if mode == "core":
rename_unlink("app-core.py", "app-express.py")
- if (app_dir / "app-core.py").exists():
- (app_dir / "app-core.py").rename(app_dir / "app.py")
+ if (dest_dir / "app-core.py").exists():
+ (dest_dir / "app-core.py").rename(dest_dir / "app.py")
+
+ return dest_dir
+
+
+def click_echo_next_steps_and_follow_up(template: ShinyTemplate):
+ for next_step in template.next_steps:
+ click.echo(f"- {next_step}")
+
+ if len(template.follow_up) > 0:
+ click.echo()
+ for follow_up in template.follow_up:
+ click.echo(cli_follow_up(follow_up))
+
+
+def cli_follow_up(follow_up: ShinyTemplateFollowUp):
+ if follow_up.type == "text":
+ return follow_up.text
+ if follow_up.type == "action":
+ return cli_action(follow_up.text)
+ if follow_up.type == "info":
+ return cli_info(follow_up.text)
+ if follow_up.type == "warning":
+ return cli_danger(follow_up.text)
+ if follow_up.type == "danger":
+ return cli_danger(follow_up.text)
- return app_dir
+ return follow_up.text
diff --git a/shiny/templates/app/01-basic-app/_template.json b/shiny/templates/app/01-basic-app/_template.json
new file mode 100644
index 000000000..0ea6216f1
--- /dev/null
+++ b/shiny/templates/app/01-basic-app/_template.json
@@ -0,0 +1,19 @@
+{
+ "type": "app",
+ "id": "basic-app",
+ "title": "Basic app",
+ "description": "A basic Shiny app template.",
+ "next_steps": [
+ "Run the app with `shiny run app.py` from the app directory."
+ ],
+ "follow_up": [
+ {
+ "type": "info",
+ "text": "Just getting started with Shiny?"
+ },
+ {
+ "type": "action",
+ "text": "Learn more at https://shiny.posit.co/py/docs/overview.html"
+ }
+ ]
+}
diff --git a/shiny/templates/app-templates/basic-app/app-core.py b/shiny/templates/app/01-basic-app/app-core.py
similarity index 100%
rename from shiny/templates/app-templates/basic-app/app-core.py
rename to shiny/templates/app/01-basic-app/app-core.py
diff --git a/shiny/templates/app-templates/basic-app/app-express.py b/shiny/templates/app/01-basic-app/app-express.py
similarity index 100%
rename from shiny/templates/app-templates/basic-app/app-express.py
rename to shiny/templates/app/01-basic-app/app-express.py
diff --git a/shiny/templates/app/02-basic-sidebar/_template.json b/shiny/templates/app/02-basic-sidebar/_template.json
new file mode 100644
index 000000000..1fdbf7efb
--- /dev/null
+++ b/shiny/templates/app/02-basic-sidebar/_template.json
@@ -0,0 +1,19 @@
+{
+ "type": "app",
+ "id": "basic-sidebar",
+ "title": "Sidebar layout",
+ "description": "An app with inputs in a sidebar and a plot in the main area.",
+ "next_steps": [
+ "Run the app with `shiny run app.py`."
+ ],
+ "follow_up": [
+ {
+ "type": "info",
+ "text": "Just getting started with Shiny?"
+ },
+ {
+ "type": "action",
+ "text": "Learn more at https://shiny.posit.co/py/docs/overview.html"
+ }
+ ]
+}
diff --git a/shiny/templates/app-templates/basic-sidebar/app-core.py b/shiny/templates/app/02-basic-sidebar/app-core.py
similarity index 100%
rename from shiny/templates/app-templates/basic-sidebar/app-core.py
rename to shiny/templates/app/02-basic-sidebar/app-core.py
diff --git a/shiny/templates/app-templates/basic-sidebar/app-express.py b/shiny/templates/app/02-basic-sidebar/app-express.py
similarity index 100%
rename from shiny/templates/app-templates/basic-sidebar/app-express.py
rename to shiny/templates/app/02-basic-sidebar/app-express.py
diff --git a/shiny/templates/app-templates/basic-navigation/penguins.csv b/shiny/templates/app/02-basic-sidebar/penguins.csv
similarity index 100%
rename from shiny/templates/app-templates/basic-navigation/penguins.csv
rename to shiny/templates/app/02-basic-sidebar/penguins.csv
diff --git a/shiny/templates/app-templates/basic-navigation/requirements.txt b/shiny/templates/app/02-basic-sidebar/requirements.txt
similarity index 100%
rename from shiny/templates/app-templates/basic-navigation/requirements.txt
rename to shiny/templates/app/02-basic-sidebar/requirements.txt
diff --git a/shiny/templates/app-templates/basic-navigation/shared.py b/shiny/templates/app/02-basic-sidebar/shared.py
similarity index 100%
rename from shiny/templates/app-templates/basic-navigation/shared.py
rename to shiny/templates/app/02-basic-sidebar/shared.py
diff --git a/shiny/templates/app/03-dashboard/_template.json b/shiny/templates/app/03-dashboard/_template.json
new file mode 100644
index 000000000..09ad15e60
--- /dev/null
+++ b/shiny/templates/app/03-dashboard/_template.json
@@ -0,0 +1,19 @@
+{
+ "type": "app",
+ "id": "dashboard",
+ "title": "Basic dashboard",
+ "description": "A basic, single page dashboard with value boxes, two plots in cards and a sidebar.",
+ "next_steps": [
+ "Run the app with `shiny run app.py`."
+ ],
+ "follow_up": [
+ {
+ "type": "info",
+ "text": "Just getting started with Shiny?"
+ },
+ {
+ "type": "action",
+ "text": "Learn more at https://shiny.posit.co/py/docs/user-interfaces.html"
+ }
+ ]
+}
diff --git a/shiny/templates/app-templates/dashboard/app-core.py b/shiny/templates/app/03-dashboard/app-core.py
similarity index 100%
rename from shiny/templates/app-templates/dashboard/app-core.py
rename to shiny/templates/app/03-dashboard/app-core.py
diff --git a/shiny/templates/app-templates/dashboard/app-express.py b/shiny/templates/app/03-dashboard/app-express.py
similarity index 100%
rename from shiny/templates/app-templates/dashboard/app-express.py
rename to shiny/templates/app/03-dashboard/app-express.py
diff --git a/shiny/templates/app-templates/basic-sidebar/penguins.csv b/shiny/templates/app/03-dashboard/penguins.csv
similarity index 100%
rename from shiny/templates/app-templates/basic-sidebar/penguins.csv
rename to shiny/templates/app/03-dashboard/penguins.csv
diff --git a/shiny/templates/app-templates/dashboard/requirements.txt b/shiny/templates/app/03-dashboard/requirements.txt
similarity index 100%
rename from shiny/templates/app-templates/dashboard/requirements.txt
rename to shiny/templates/app/03-dashboard/requirements.txt
diff --git a/shiny/templates/app-templates/basic-sidebar/shared.py b/shiny/templates/app/03-dashboard/shared.py
similarity index 100%
rename from shiny/templates/app-templates/basic-sidebar/shared.py
rename to shiny/templates/app/03-dashboard/shared.py
diff --git a/shiny/templates/app-templates/dashboard/styles.css b/shiny/templates/app/03-dashboard/styles.css
similarity index 100%
rename from shiny/templates/app-templates/dashboard/styles.css
rename to shiny/templates/app/03-dashboard/styles.css
diff --git a/shiny/templates/app-templates/dashboard-tips/README.md b/shiny/templates/app/04-dashboard-tips/README.md
similarity index 100%
rename from shiny/templates/app-templates/dashboard-tips/README.md
rename to shiny/templates/app/04-dashboard-tips/README.md
diff --git a/shiny/templates/app/04-dashboard-tips/_template.json b/shiny/templates/app/04-dashboard-tips/_template.json
new file mode 100644
index 000000000..0c514ac0d
--- /dev/null
+++ b/shiny/templates/app/04-dashboard-tips/_template.json
@@ -0,0 +1,19 @@
+{
+ "type": "app",
+ "id": "dashboard-tips",
+ "title": "Intermediate dashboard",
+ "description": "An intermediate dashboard with value boxes, several plots in cards and a sidebar.",
+ "next_steps": [
+ "Run the app with `shiny run app.py`."
+ ],
+ "follow_up": [
+ {
+ "type": "info",
+ "text": "Just getting started with Shiny?"
+ },
+ {
+ "type": "action",
+ "text": "Learn more at https://shiny.posit.co/py/docs/user-interfaces.html"
+ }
+ ]
+}
diff --git a/shiny/templates/app-templates/dashboard-tips/app-core.py b/shiny/templates/app/04-dashboard-tips/app-core.py
similarity index 100%
rename from shiny/templates/app-templates/dashboard-tips/app-core.py
rename to shiny/templates/app/04-dashboard-tips/app-core.py
diff --git a/shiny/templates/app-templates/dashboard-tips/app-express.py b/shiny/templates/app/04-dashboard-tips/app-express.py
similarity index 100%
rename from shiny/templates/app-templates/dashboard-tips/app-express.py
rename to shiny/templates/app/04-dashboard-tips/app-express.py
diff --git a/shiny/templates/app-templates/dashboard-tips/requirements.txt b/shiny/templates/app/04-dashboard-tips/requirements.txt
similarity index 100%
rename from shiny/templates/app-templates/dashboard-tips/requirements.txt
rename to shiny/templates/app/04-dashboard-tips/requirements.txt
diff --git a/shiny/templates/app-templates/dashboard-tips/shared.py b/shiny/templates/app/04-dashboard-tips/shared.py
similarity index 100%
rename from shiny/templates/app-templates/dashboard-tips/shared.py
rename to shiny/templates/app/04-dashboard-tips/shared.py
diff --git a/shiny/templates/app-templates/dashboard-tips/styles.css b/shiny/templates/app/04-dashboard-tips/styles.css
similarity index 100%
rename from shiny/templates/app-templates/dashboard-tips/styles.css
rename to shiny/templates/app/04-dashboard-tips/styles.css
diff --git a/shiny/templates/app-templates/dashboard-tips/tips.csv b/shiny/templates/app/04-dashboard-tips/tips.csv
similarity index 100%
rename from shiny/templates/app-templates/dashboard-tips/tips.csv
rename to shiny/templates/app/04-dashboard-tips/tips.csv
diff --git a/shiny/templates/app/05-basic-navigation/_template.json b/shiny/templates/app/05-basic-navigation/_template.json
new file mode 100644
index 000000000..9d530fcca
--- /dev/null
+++ b/shiny/templates/app/05-basic-navigation/_template.json
@@ -0,0 +1,19 @@
+{
+ "type": "app",
+ "id": "basic-navigation",
+ "title": "Navigating multiple pages/panels",
+ "description": "An app with a top navigation bar and two pages.",
+ "next_steps": [
+ "Run the app with `shiny run app.py`."
+ ],
+ "follow_up": [
+ {
+ "type": "info",
+ "text": "Just getting started with Shiny?"
+ },
+ {
+ "type": "action",
+ "text": "Learn more at https://shiny.posit.co/py/docs/user-interfaces.html"
+ }
+ ]
+}
diff --git a/shiny/templates/app-templates/basic-navigation/app-core.py b/shiny/templates/app/05-basic-navigation/app-core.py
similarity index 100%
rename from shiny/templates/app-templates/basic-navigation/app-core.py
rename to shiny/templates/app/05-basic-navigation/app-core.py
diff --git a/shiny/templates/app-templates/basic-navigation/app-express.py b/shiny/templates/app/05-basic-navigation/app-express.py
similarity index 100%
rename from shiny/templates/app-templates/basic-navigation/app-express.py
rename to shiny/templates/app/05-basic-navigation/app-express.py
diff --git a/shiny/templates/app-templates/dashboard/penguins.csv b/shiny/templates/app/05-basic-navigation/penguins.csv
similarity index 100%
rename from shiny/templates/app-templates/dashboard/penguins.csv
rename to shiny/templates/app/05-basic-navigation/penguins.csv
diff --git a/shiny/templates/app-templates/basic-sidebar/requirements.txt b/shiny/templates/app/05-basic-navigation/requirements.txt
similarity index 100%
rename from shiny/templates/app-templates/basic-sidebar/requirements.txt
rename to shiny/templates/app/05-basic-navigation/requirements.txt
diff --git a/shiny/templates/app-templates/dashboard/shared.py b/shiny/templates/app/05-basic-navigation/shared.py
similarity index 100%
rename from shiny/templates/app-templates/dashboard/shared.py
rename to shiny/templates/app/05-basic-navigation/shared.py
diff --git a/shiny/templates/chat/enterprise/aws-bedrock-anthropic/_template.json b/shiny/templates/chat/enterprise/aws-bedrock-anthropic/_template.json
new file mode 100644
index 000000000..39712d0f7
--- /dev/null
+++ b/shiny/templates/chat/enterprise/aws-bedrock-anthropic/_template.json
@@ -0,0 +1,5 @@
+{
+ "type": "app",
+ "id": "chat-ai-anthropic-aws",
+ "title": "Chat AI using Anthropic via AWS Bedrock"
+}
diff --git a/examples/chat/enterprise/aws-bedrock-anthropic/app.py b/shiny/templates/chat/enterprise/aws-bedrock-anthropic/app.py
similarity index 100%
rename from examples/chat/enterprise/aws-bedrock-anthropic/app.py
rename to shiny/templates/chat/enterprise/aws-bedrock-anthropic/app.py
diff --git a/examples/chat/enterprise/aws-bedrock-anthropic/app_utils.py b/shiny/templates/chat/enterprise/aws-bedrock-anthropic/app_utils.py
similarity index 100%
rename from examples/chat/enterprise/aws-bedrock-anthropic/app_utils.py
rename to shiny/templates/chat/enterprise/aws-bedrock-anthropic/app_utils.py
diff --git a/examples/chat/enterprise/aws-bedrock-anthropic/requirements.txt b/shiny/templates/chat/enterprise/aws-bedrock-anthropic/requirements.txt
similarity index 100%
rename from examples/chat/enterprise/aws-bedrock-anthropic/requirements.txt
rename to shiny/templates/chat/enterprise/aws-bedrock-anthropic/requirements.txt
diff --git a/shiny/templates/chat/enterprise/azure-openai/_template.json b/shiny/templates/chat/enterprise/azure-openai/_template.json
new file mode 100644
index 000000000..14702f9c7
--- /dev/null
+++ b/shiny/templates/chat/enterprise/azure-openai/_template.json
@@ -0,0 +1,5 @@
+{
+ "type": "app",
+ "id": "chat-ai-azure-openai",
+ "title": "Chat AI using OpenAI via Azure"
+}
diff --git a/examples/chat/enterprise/azure-openai/app.py b/shiny/templates/chat/enterprise/azure-openai/app.py
similarity index 100%
rename from examples/chat/enterprise/azure-openai/app.py
rename to shiny/templates/chat/enterprise/azure-openai/app.py
diff --git a/examples/chat/enterprise/azure-openai/app_utils.py b/shiny/templates/chat/enterprise/azure-openai/app_utils.py
similarity index 100%
rename from examples/chat/enterprise/azure-openai/app_utils.py
rename to shiny/templates/chat/enterprise/azure-openai/app_utils.py
diff --git a/examples/chat/enterprise/azure-openai/requirements.txt b/shiny/templates/chat/enterprise/azure-openai/requirements.txt
similarity index 100%
rename from examples/chat/enterprise/azure-openai/requirements.txt
rename to shiny/templates/chat/enterprise/azure-openai/requirements.txt
diff --git a/shiny/templates/chat/hello-providers/anthropic/_template.json b/shiny/templates/chat/hello-providers/anthropic/_template.json
new file mode 100644
index 000000000..79e2bf257
--- /dev/null
+++ b/shiny/templates/chat/hello-providers/anthropic/_template.json
@@ -0,0 +1,5 @@
+{
+ "type": "app",
+ "id": "chat-ai-anthropic",
+ "title": "Chat AI using Anthropic"
+}
diff --git a/shiny/templates/chat/hello-providers/anthropic/app.py b/shiny/templates/chat/hello-providers/anthropic/app.py
new file mode 100644
index 000000000..9c7653ea3
--- /dev/null
+++ b/shiny/templates/chat/hello-providers/anthropic/app.py
@@ -0,0 +1,44 @@
+# ------------------------------------------------------------------------------------
+# A basic Shiny Chat example powered by Anthropic's Claude model.
+# To run it, you'll need an Anthropic API key.
+# To get one, follow the instructions at https://docs.anthropic.com/en/api/getting-started
+# ------------------------------------------------------------------------------------
+import os
+
+from anthropic import AsyncAnthropic
+from app_utils import load_dotenv
+
+from shiny.express import ui
+
+# Either explicitly set the ANTHROPIC_API_KEY environment variable before launching the
+# app, or set them in a file named `.env`. The `python-dotenv` package will load `.env`
+# as environment variables which can later be read by `os.getenv()`.
+load_dotenv()
+llm = AsyncAnthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
+
+# Set some Shiny page options
+ui.page_opts(
+ title="Hello Anthropic Claude Chat",
+ fillable=True,
+ fillable_mobile=True,
+)
+
+# Create and display empty chat
+chat = ui.Chat(id="chat")
+chat.ui()
+
+
+# Define a callback to run when the user submits a message
+@chat.on_user_submit
+async def _():
+ # Get messages currently in the chat
+ messages = chat.messages(format="anthropic")
+ # Create a response message stream
+ response = await llm.messages.create(
+ model="claude-3-opus-20240229",
+ messages=messages,
+ stream=True,
+ max_tokens=1000,
+ )
+ # Append the response stream into the chat
+ await chat.append_message_stream(response)
diff --git a/examples/chat/hello-providers/anthropic/app_utils.py b/shiny/templates/chat/hello-providers/anthropic/app_utils.py
similarity index 100%
rename from examples/chat/hello-providers/anthropic/app_utils.py
rename to shiny/templates/chat/hello-providers/anthropic/app_utils.py
diff --git a/examples/chat/hello-providers/anthropic/requirements.txt b/shiny/templates/chat/hello-providers/anthropic/requirements.txt
similarity index 100%
rename from examples/chat/hello-providers/anthropic/requirements.txt
rename to shiny/templates/chat/hello-providers/anthropic/requirements.txt
diff --git a/shiny/templates/chat/hello-providers/gemini/_template.json b/shiny/templates/chat/hello-providers/gemini/_template.json
new file mode 100644
index 000000000..baf30e7cd
--- /dev/null
+++ b/shiny/templates/chat/hello-providers/gemini/_template.json
@@ -0,0 +1,5 @@
+{
+ "type": "app",
+ "id": "chat-ai-gemini",
+ "title": "Chat AI using Google Gemini"
+}
diff --git a/examples/chat/hello-providers/gemini/app.py b/shiny/templates/chat/hello-providers/gemini/app.py
similarity index 100%
rename from examples/chat/hello-providers/gemini/app.py
rename to shiny/templates/chat/hello-providers/gemini/app.py
diff --git a/examples/chat/hello-providers/gemini/app_utils.py b/shiny/templates/chat/hello-providers/gemini/app_utils.py
similarity index 100%
rename from examples/chat/hello-providers/gemini/app_utils.py
rename to shiny/templates/chat/hello-providers/gemini/app_utils.py
diff --git a/examples/chat/hello-providers/gemini/requirements.txt b/shiny/templates/chat/hello-providers/gemini/requirements.txt
similarity index 100%
rename from examples/chat/hello-providers/gemini/requirements.txt
rename to shiny/templates/chat/hello-providers/gemini/requirements.txt
diff --git a/shiny/templates/chat/hello-providers/langchain/_template.json b/shiny/templates/chat/hello-providers/langchain/_template.json
new file mode 100644
index 000000000..3ac04a285
--- /dev/null
+++ b/shiny/templates/chat/hello-providers/langchain/_template.json
@@ -0,0 +1,5 @@
+{
+ "type": "app",
+ "id": "chat-ai-langchain",
+ "title": "Chat AI using LangChain"
+}
diff --git a/examples/chat/hello-providers/langchain/app.py b/shiny/templates/chat/hello-providers/langchain/app.py
similarity index 100%
rename from examples/chat/hello-providers/langchain/app.py
rename to shiny/templates/chat/hello-providers/langchain/app.py
diff --git a/examples/chat/hello-providers/langchain/app_utils.py b/shiny/templates/chat/hello-providers/langchain/app_utils.py
similarity index 100%
rename from examples/chat/hello-providers/langchain/app_utils.py
rename to shiny/templates/chat/hello-providers/langchain/app_utils.py
diff --git a/examples/chat/hello-providers/langchain/requirements.txt b/shiny/templates/chat/hello-providers/langchain/requirements.txt
similarity index 100%
rename from examples/chat/hello-providers/langchain/requirements.txt
rename to shiny/templates/chat/hello-providers/langchain/requirements.txt
diff --git a/shiny/templates/chat/hello-providers/ollama/_template.json b/shiny/templates/chat/hello-providers/ollama/_template.json
new file mode 100644
index 000000000..9a1c53ccc
--- /dev/null
+++ b/shiny/templates/chat/hello-providers/ollama/_template.json
@@ -0,0 +1,5 @@
+{
+ "type": "app",
+ "id": "chat-ai-ollama",
+ "title": "Chat AI using Ollama"
+}
diff --git a/examples/chat/hello-providers/ollama/app.py b/shiny/templates/chat/hello-providers/ollama/app.py
similarity index 100%
rename from examples/chat/hello-providers/ollama/app.py
rename to shiny/templates/chat/hello-providers/ollama/app.py
diff --git a/examples/chat/hello-providers/ollama/requirements.txt b/shiny/templates/chat/hello-providers/ollama/requirements.txt
similarity index 100%
rename from examples/chat/hello-providers/ollama/requirements.txt
rename to shiny/templates/chat/hello-providers/ollama/requirements.txt
diff --git a/shiny/templates/chat/hello-providers/openai/_template.json b/shiny/templates/chat/hello-providers/openai/_template.json
new file mode 100644
index 000000000..89bfb15d3
--- /dev/null
+++ b/shiny/templates/chat/hello-providers/openai/_template.json
@@ -0,0 +1,5 @@
+{
+ "type": "app",
+ "id": "chat-ai-openai",
+ "title": "Chat AI using OpenAI"
+}
diff --git a/examples/chat/hello-providers/openai/app.py b/shiny/templates/chat/hello-providers/openai/app.py
similarity index 100%
rename from examples/chat/hello-providers/openai/app.py
rename to shiny/templates/chat/hello-providers/openai/app.py
diff --git a/examples/chat/hello-providers/openai/app_utils.py b/shiny/templates/chat/hello-providers/openai/app_utils.py
similarity index 100%
rename from examples/chat/hello-providers/openai/app_utils.py
rename to shiny/templates/chat/hello-providers/openai/app_utils.py
diff --git a/examples/chat/hello-providers/openai/requirements.txt b/shiny/templates/chat/hello-providers/openai/requirements.txt
similarity index 100%
rename from examples/chat/hello-providers/openai/requirements.txt
rename to shiny/templates/chat/hello-providers/openai/requirements.txt
diff --git a/shiny/templates/package-templates/js-input/custom_component/distjs/index.js b/shiny/templates/package-templates/js-input/custom_component/distjs/index.js
deleted file mode 100644
index 620c5999a..000000000
--- a/shiny/templates/package-templates/js-input/custom_component/distjs/index.js
+++ /dev/null
@@ -1,814 +0,0 @@
-"use strict";
-(() => {
- var __defProp = Object.defineProperty;
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
- var __decorateClass = (decorators, target, key, kind) => {
- var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
- for (var i4 = decorators.length - 1, decorator; i4 >= 0; i4--)
- if (decorator = decorators[i4])
- result = (kind ? decorator(target, key, result) : decorator(result)) || result;
- if (kind && result)
- __defProp(target, key, result);
- return result;
- };
-
- // node_modules/@lit/reactive-element/css-tag.js
- var t = globalThis;
- var e = t.ShadowRoot && (void 0 === t.ShadyCSS || t.ShadyCSS.nativeShadow) && "adoptedStyleSheets" in Document.prototype && "replace" in CSSStyleSheet.prototype;
- var s = Symbol();
- var o = /* @__PURE__ */ new WeakMap();
- var n = class {
- constructor(t4, e5, o5) {
- if (this._$cssResult$ = true, o5 !== s)
- throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");
- this.cssText = t4, this.t = e5;
- }
- get styleSheet() {
- let t4 = this.o;
- const s4 = this.t;
- if (e && void 0 === t4) {
- const e5 = void 0 !== s4 && 1 === s4.length;
- e5 && (t4 = o.get(s4)), void 0 === t4 && ((this.o = t4 = new CSSStyleSheet()).replaceSync(this.cssText), e5 && o.set(s4, t4));
- }
- return t4;
- }
- toString() {
- return this.cssText;
- }
- };
- var r = (t4) => new n("string" == typeof t4 ? t4 : t4 + "", void 0, s);
- var i = (t4, ...e5) => {
- const o5 = 1 === t4.length ? t4[0] : e5.reduce((e6, s4, o6) => e6 + ((t5) => {
- if (true === t5._$cssResult$)
- return t5.cssText;
- if ("number" == typeof t5)
- return t5;
- throw Error("Value passed to 'css' function must be a 'css' function result: " + t5 + ". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.");
- })(s4) + t4[o6 + 1], t4[0]);
- return new n(o5, t4, s);
- };
- var S = (s4, o5) => {
- if (e)
- s4.adoptedStyleSheets = o5.map((t4) => t4 instanceof CSSStyleSheet ? t4 : t4.styleSheet);
- else
- for (const e5 of o5) {
- const o6 = document.createElement("style"), n5 = t.litNonce;
- void 0 !== n5 && o6.setAttribute("nonce", n5), o6.textContent = e5.cssText, s4.appendChild(o6);
- }
- };
- var c = e ? (t4) => t4 : (t4) => t4 instanceof CSSStyleSheet ? ((t5) => {
- let e5 = "";
- for (const s4 of t5.cssRules)
- e5 += s4.cssText;
- return r(e5);
- })(t4) : t4;
-
- // node_modules/@lit/reactive-element/reactive-element.js
- var { is: i2, defineProperty: e2, getOwnPropertyDescriptor: r2, getOwnPropertyNames: h, getOwnPropertySymbols: o2, getPrototypeOf: n2 } = Object;
- var a = globalThis;
- var c2 = a.trustedTypes;
- var l = c2 ? c2.emptyScript : "";
- var p = a.reactiveElementPolyfillSupport;
- var d = (t4, s4) => t4;
- var u = { toAttribute(t4, s4) {
- switch (s4) {
- case Boolean:
- t4 = t4 ? l : null;
- break;
- case Object:
- case Array:
- t4 = null == t4 ? t4 : JSON.stringify(t4);
- }
- return t4;
- }, fromAttribute(t4, s4) {
- let i4 = t4;
- switch (s4) {
- case Boolean:
- i4 = null !== t4;
- break;
- case Number:
- i4 = null === t4 ? null : Number(t4);
- break;
- case Object:
- case Array:
- try {
- i4 = JSON.parse(t4);
- } catch (t5) {
- i4 = null;
- }
- }
- return i4;
- } };
- var f = (t4, s4) => !i2(t4, s4);
- var y = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f };
- Symbol.metadata ??= Symbol("metadata"), a.litPropertyMetadata ??= /* @__PURE__ */ new WeakMap();
- var b = class extends HTMLElement {
- static addInitializer(t4) {
- this._$Ei(), (this.l ??= []).push(t4);
- }
- static get observedAttributes() {
- return this.finalize(), this._$Eh && [...this._$Eh.keys()];
- }
- static createProperty(t4, s4 = y) {
- if (s4.state && (s4.attribute = false), this._$Ei(), this.elementProperties.set(t4, s4), !s4.noAccessor) {
- const i4 = Symbol(), r6 = this.getPropertyDescriptor(t4, i4, s4);
- void 0 !== r6 && e2(this.prototype, t4, r6);
- }
- }
- static getPropertyDescriptor(t4, s4, i4) {
- const { get: e5, set: h3 } = r2(this.prototype, t4) ?? { get() {
- return this[s4];
- }, set(t5) {
- this[s4] = t5;
- } };
- return { get() {
- return e5?.call(this);
- }, set(s5) {
- const r6 = e5?.call(this);
- h3.call(this, s5), this.requestUpdate(t4, r6, i4);
- }, configurable: true, enumerable: true };
- }
- static getPropertyOptions(t4) {
- return this.elementProperties.get(t4) ?? y;
- }
- static _$Ei() {
- if (this.hasOwnProperty(d("elementProperties")))
- return;
- const t4 = n2(this);
- t4.finalize(), void 0 !== t4.l && (this.l = [...t4.l]), this.elementProperties = new Map(t4.elementProperties);
- }
- static finalize() {
- if (this.hasOwnProperty(d("finalized")))
- return;
- if (this.finalized = true, this._$Ei(), this.hasOwnProperty(d("properties"))) {
- const t5 = this.properties, s4 = [...h(t5), ...o2(t5)];
- for (const i4 of s4)
- this.createProperty(i4, t5[i4]);
- }
- const t4 = this[Symbol.metadata];
- if (null !== t4) {
- const s4 = litPropertyMetadata.get(t4);
- if (void 0 !== s4)
- for (const [t5, i4] of s4)
- this.elementProperties.set(t5, i4);
- }
- this._$Eh = /* @__PURE__ */ new Map();
- for (const [t5, s4] of this.elementProperties) {
- const i4 = this._$Eu(t5, s4);
- void 0 !== i4 && this._$Eh.set(i4, t5);
- }
- this.elementStyles = this.finalizeStyles(this.styles);
- }
- static finalizeStyles(s4) {
- const i4 = [];
- if (Array.isArray(s4)) {
- const e5 = new Set(s4.flat(1 / 0).reverse());
- for (const s5 of e5)
- i4.unshift(c(s5));
- } else
- void 0 !== s4 && i4.push(c(s4));
- return i4;
- }
- static _$Eu(t4, s4) {
- const i4 = s4.attribute;
- return false === i4 ? void 0 : "string" == typeof i4 ? i4 : "string" == typeof t4 ? t4.toLowerCase() : void 0;
- }
- constructor() {
- super(), this._$Ep = void 0, this.isUpdatePending = false, this.hasUpdated = false, this._$Em = null, this._$Ev();
- }
- _$Ev() {
- this._$Eg = new Promise((t4) => this.enableUpdating = t4), this._$AL = /* @__PURE__ */ new Map(), this._$ES(), this.requestUpdate(), this.constructor.l?.forEach((t4) => t4(this));
- }
- addController(t4) {
- (this._$E_ ??= /* @__PURE__ */ new Set()).add(t4), void 0 !== this.renderRoot && this.isConnected && t4.hostConnected?.();
- }
- removeController(t4) {
- this._$E_?.delete(t4);
- }
- _$ES() {
- const t4 = /* @__PURE__ */ new Map(), s4 = this.constructor.elementProperties;
- for (const i4 of s4.keys())
- this.hasOwnProperty(i4) && (t4.set(i4, this[i4]), delete this[i4]);
- t4.size > 0 && (this._$Ep = t4);
- }
- createRenderRoot() {
- const t4 = this.shadowRoot ?? this.attachShadow(this.constructor.shadowRootOptions);
- return S(t4, this.constructor.elementStyles), t4;
- }
- connectedCallback() {
- this.renderRoot ??= this.createRenderRoot(), this.enableUpdating(true), this._$E_?.forEach((t4) => t4.hostConnected?.());
- }
- enableUpdating(t4) {
- }
- disconnectedCallback() {
- this._$E_?.forEach((t4) => t4.hostDisconnected?.());
- }
- attributeChangedCallback(t4, s4, i4) {
- this._$AK(t4, i4);
- }
- _$EO(t4, s4) {
- const i4 = this.constructor.elementProperties.get(t4), e5 = this.constructor._$Eu(t4, i4);
- if (void 0 !== e5 && true === i4.reflect) {
- const r6 = (void 0 !== i4.converter?.toAttribute ? i4.converter : u).toAttribute(s4, i4.type);
- this._$Em = t4, null == r6 ? this.removeAttribute(e5) : this.setAttribute(e5, r6), this._$Em = null;
- }
- }
- _$AK(t4, s4) {
- const i4 = this.constructor, e5 = i4._$Eh.get(t4);
- if (void 0 !== e5 && this._$Em !== e5) {
- const t5 = i4.getPropertyOptions(e5), r6 = "function" == typeof t5.converter ? { fromAttribute: t5.converter } : void 0 !== t5.converter?.fromAttribute ? t5.converter : u;
- this._$Em = e5, this[e5] = r6.fromAttribute(s4, t5.type), this._$Em = null;
- }
- }
- requestUpdate(t4, s4, i4, e5 = false, r6) {
- if (void 0 !== t4) {
- if (i4 ??= this.constructor.getPropertyOptions(t4), !(i4.hasChanged ?? f)(e5 ? r6 : this[t4], s4))
- return;
- this.C(t4, s4, i4);
- }
- false === this.isUpdatePending && (this._$Eg = this._$EP());
- }
- C(t4, s4, i4) {
- this._$AL.has(t4) || this._$AL.set(t4, s4), true === i4.reflect && this._$Em !== t4 && (this._$Ej ??= /* @__PURE__ */ new Set()).add(t4);
- }
- async _$EP() {
- this.isUpdatePending = true;
- try {
- await this._$Eg;
- } catch (t5) {
- Promise.reject(t5);
- }
- const t4 = this.scheduleUpdate();
- return null != t4 && await t4, !this.isUpdatePending;
- }
- scheduleUpdate() {
- return this.performUpdate();
- }
- performUpdate() {
- if (!this.isUpdatePending)
- return;
- if (!this.hasUpdated) {
- if (this.renderRoot ??= this.createRenderRoot(), this._$Ep) {
- for (const [t6, s5] of this._$Ep)
- this[t6] = s5;
- this._$Ep = void 0;
- }
- const t5 = this.constructor.elementProperties;
- if (t5.size > 0)
- for (const [s5, i4] of t5)
- true !== i4.wrapped || this._$AL.has(s5) || void 0 === this[s5] || this.C(s5, this[s5], i4);
- }
- let t4 = false;
- const s4 = this._$AL;
- try {
- t4 = this.shouldUpdate(s4), t4 ? (this.willUpdate(s4), this._$E_?.forEach((t5) => t5.hostUpdate?.()), this.update(s4)) : this._$ET();
- } catch (s5) {
- throw t4 = false, this._$ET(), s5;
- }
- t4 && this._$AE(s4);
- }
- willUpdate(t4) {
- }
- _$AE(t4) {
- this._$E_?.forEach((t5) => t5.hostUpdated?.()), this.hasUpdated || (this.hasUpdated = true, this.firstUpdated(t4)), this.updated(t4);
- }
- _$ET() {
- this._$AL = /* @__PURE__ */ new Map(), this.isUpdatePending = false;
- }
- get updateComplete() {
- return this.getUpdateComplete();
- }
- getUpdateComplete() {
- return this._$Eg;
- }
- shouldUpdate(t4) {
- return true;
- }
- update(t4) {
- this._$Ej &&= this._$Ej.forEach((t5) => this._$EO(t5, this[t5])), this._$ET();
- }
- updated(t4) {
- }
- firstUpdated(t4) {
- }
- };
- b.elementStyles = [], b.shadowRootOptions = { mode: "open" }, b[d("elementProperties")] = /* @__PURE__ */ new Map(), b[d("finalized")] = /* @__PURE__ */ new Map(), p?.({ ReactiveElement: b }), (a.reactiveElementVersions ??= []).push("2.0.2");
-
- // node_modules/lit-html/lit-html.js
- var t2 = globalThis;
- var i3 = t2.trustedTypes;
- var s2 = i3 ? i3.createPolicy("lit-html", { createHTML: (t4) => t4 }) : void 0;
- var e3 = "$lit$";
- var h2 = `lit$${(Math.random() + "").slice(9)}$`;
- var o3 = "?" + h2;
- var n3 = `<${o3}>`;
- var r3 = document;
- var l2 = () => r3.createComment("");
- var c3 = (t4) => null === t4 || "object" != typeof t4 && "function" != typeof t4;
- var a2 = Array.isArray;
- var u2 = (t4) => a2(t4) || "function" == typeof t4?.[Symbol.iterator];
- var d2 = "[ \n\f\r]";
- var f2 = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g;
- var v = /-->/g;
- var _ = />/g;
- var m = RegExp(`>|${d2}(?:([^\\s"'>=/]+)(${d2}*=${d2}*(?:[^
-\f\r"'\`<>=]|("|')|))|$)`, "g");
- var p2 = /'/g;
- var g = /"/g;
- var $2 = /^(?:script|style|textarea|title)$/i;
- var y2 = (t4) => (i4, ...s4) => ({ _$litType$: t4, strings: i4, values: s4 });
- var x = y2(1);
- var b2 = y2(2);
- var w = Symbol.for("lit-noChange");
- var T = Symbol.for("lit-nothing");
- var A = /* @__PURE__ */ new WeakMap();
- var E = r3.createTreeWalker(r3, 129);
- function C(t4, i4) {
- if (!Array.isArray(t4) || !t4.hasOwnProperty("raw"))
- throw Error("invalid template strings array");
- return void 0 !== s2 ? s2.createHTML(i4) : i4;
- }
- var P = (t4, i4) => {
- const s4 = t4.length - 1, o5 = [];
- let r6, l3 = 2 === i4 ? "" : "")), o5];
- };
- var V = class _V {
- constructor({ strings: t4, _$litType$: s4 }, n5) {
- let r6;
- this.parts = [];
- let c4 = 0, a3 = 0;
- const u3 = t4.length - 1, d3 = this.parts, [f3, v2] = P(t4, s4);
- if (this.el = _V.createElement(f3, n5), E.currentNode = this.el.content, 2 === s4) {
- const t5 = this.el.content.firstChild;
- t5.replaceWith(...t5.childNodes);
- }
- for (; null !== (r6 = E.nextNode()) && d3.length < u3; ) {
- if (1 === r6.nodeType) {
- if (r6.hasAttributes())
- for (const t5 of r6.getAttributeNames())
- if (t5.endsWith(e3)) {
- const i4 = v2[a3++], s5 = r6.getAttribute(t5).split(h2), e5 = /([.?@])?(.*)/.exec(i4);
- d3.push({ type: 1, index: c4, name: e5[2], strings: s5, ctor: "." === e5[1] ? k : "?" === e5[1] ? H : "@" === e5[1] ? I : R }), r6.removeAttribute(t5);
- } else
- t5.startsWith(h2) && (d3.push({ type: 6, index: c4 }), r6.removeAttribute(t5));
- if ($2.test(r6.tagName)) {
- const t5 = r6.textContent.split(h2), s5 = t5.length - 1;
- if (s5 > 0) {
- r6.textContent = i3 ? i3.emptyScript : "";
- for (let i4 = 0; i4 < s5; i4++)
- r6.append(t5[i4], l2()), E.nextNode(), d3.push({ type: 2, index: ++c4 });
- r6.append(t5[s5], l2());
- }
- }
- } else if (8 === r6.nodeType)
- if (r6.data === o3)
- d3.push({ type: 2, index: c4 });
- else {
- let t5 = -1;
- for (; -1 !== (t5 = r6.data.indexOf(h2, t5 + 1)); )
- d3.push({ type: 7, index: c4 }), t5 += h2.length - 1;
- }
- c4++;
- }
- }
- static createElement(t4, i4) {
- const s4 = r3.createElement("template");
- return s4.innerHTML = t4, s4;
- }
- };
- function N(t4, i4, s4 = t4, e5) {
- if (i4 === w)
- return i4;
- let h3 = void 0 !== e5 ? s4._$Co?.[e5] : s4._$Cl;
- const o5 = c3(i4) ? void 0 : i4._$litDirective$;
- return h3?.constructor !== o5 && (h3?._$AO?.(false), void 0 === o5 ? h3 = void 0 : (h3 = new o5(t4), h3._$AT(t4, s4, e5)), void 0 !== e5 ? (s4._$Co ??= [])[e5] = h3 : s4._$Cl = h3), void 0 !== h3 && (i4 = N(t4, h3._$AS(t4, i4.values), h3, e5)), i4;
- }
- var S2 = class {
- constructor(t4, i4) {
- this._$AV = [], this._$AN = void 0, this._$AD = t4, this._$AM = i4;
- }
- get parentNode() {
- return this._$AM.parentNode;
- }
- get _$AU() {
- return this._$AM._$AU;
- }
- u(t4) {
- const { el: { content: i4 }, parts: s4 } = this._$AD, e5 = (t4?.creationScope ?? r3).importNode(i4, true);
- E.currentNode = e5;
- let h3 = E.nextNode(), o5 = 0, n5 = 0, l3 = s4[0];
- for (; void 0 !== l3; ) {
- if (o5 === l3.index) {
- let i5;
- 2 === l3.type ? i5 = new M(h3, h3.nextSibling, this, t4) : 1 === l3.type ? i5 = new l3.ctor(h3, l3.name, l3.strings, this, t4) : 6 === l3.type && (i5 = new L(h3, this, t4)), this._$AV.push(i5), l3 = s4[++n5];
- }
- o5 !== l3?.index && (h3 = E.nextNode(), o5++);
- }
- return E.currentNode = r3, e5;
- }
- p(t4) {
- let i4 = 0;
- for (const s4 of this._$AV)
- void 0 !== s4 && (void 0 !== s4.strings ? (s4._$AI(t4, s4, i4), i4 += s4.strings.length - 2) : s4._$AI(t4[i4])), i4++;
- }
- };
- var M = class _M {
- get _$AU() {
- return this._$AM?._$AU ?? this._$Cv;
- }
- constructor(t4, i4, s4, e5) {
- this.type = 2, this._$AH = T, this._$AN = void 0, this._$AA = t4, this._$AB = i4, this._$AM = s4, this.options = e5, this._$Cv = e5?.isConnected ?? true;
- }
- get parentNode() {
- let t4 = this._$AA.parentNode;
- const i4 = this._$AM;
- return void 0 !== i4 && 11 === t4?.nodeType && (t4 = i4.parentNode), t4;
- }
- get startNode() {
- return this._$AA;
- }
- get endNode() {
- return this._$AB;
- }
- _$AI(t4, i4 = this) {
- t4 = N(this, t4, i4), c3(t4) ? t4 === T || null == t4 || "" === t4 ? (this._$AH !== T && this._$AR(), this._$AH = T) : t4 !== this._$AH && t4 !== w && this._(t4) : void 0 !== t4._$litType$ ? this.g(t4) : void 0 !== t4.nodeType ? this.$(t4) : u2(t4) ? this.T(t4) : this._(t4);
- }
- k(t4) {
- return this._$AA.parentNode.insertBefore(t4, this._$AB);
- }
- $(t4) {
- this._$AH !== t4 && (this._$AR(), this._$AH = this.k(t4));
- }
- _(t4) {
- this._$AH !== T && c3(this._$AH) ? this._$AA.nextSibling.data = t4 : this.$(r3.createTextNode(t4)), this._$AH = t4;
- }
- g(t4) {
- const { values: i4, _$litType$: s4 } = t4, e5 = "number" == typeof s4 ? this._$AC(t4) : (void 0 === s4.el && (s4.el = V.createElement(C(s4.h, s4.h[0]), this.options)), s4);
- if (this._$AH?._$AD === e5)
- this._$AH.p(i4);
- else {
- const t5 = new S2(e5, this), s5 = t5.u(this.options);
- t5.p(i4), this.$(s5), this._$AH = t5;
- }
- }
- _$AC(t4) {
- let i4 = A.get(t4.strings);
- return void 0 === i4 && A.set(t4.strings, i4 = new V(t4)), i4;
- }
- T(t4) {
- a2(this._$AH) || (this._$AH = [], this._$AR());
- const i4 = this._$AH;
- let s4, e5 = 0;
- for (const h3 of t4)
- e5 === i4.length ? i4.push(s4 = new _M(this.k(l2()), this.k(l2()), this, this.options)) : s4 = i4[e5], s4._$AI(h3), e5++;
- e5 < i4.length && (this._$AR(s4 && s4._$AB.nextSibling, e5), i4.length = e5);
- }
- _$AR(t4 = this._$AA.nextSibling, i4) {
- for (this._$AP?.(false, true, i4); t4 && t4 !== this._$AB; ) {
- const i5 = t4.nextSibling;
- t4.remove(), t4 = i5;
- }
- }
- setConnected(t4) {
- void 0 === this._$AM && (this._$Cv = t4, this._$AP?.(t4));
- }
- };
- var R = class {
- get tagName() {
- return this.element.tagName;
- }
- get _$AU() {
- return this._$AM._$AU;
- }
- constructor(t4, i4, s4, e5, h3) {
- this.type = 1, this._$AH = T, this._$AN = void 0, this.element = t4, this.name = i4, this._$AM = e5, this.options = h3, s4.length > 2 || "" !== s4[0] || "" !== s4[1] ? (this._$AH = Array(s4.length - 1).fill(new String()), this.strings = s4) : this._$AH = T;
- }
- _$AI(t4, i4 = this, s4, e5) {
- const h3 = this.strings;
- let o5 = false;
- if (void 0 === h3)
- t4 = N(this, t4, i4, 0), o5 = !c3(t4) || t4 !== this._$AH && t4 !== w, o5 && (this._$AH = t4);
- else {
- const e6 = t4;
- let n5, r6;
- for (t4 = h3[0], n5 = 0; n5 < h3.length - 1; n5++)
- r6 = N(this, e6[s4 + n5], i4, n5), r6 === w && (r6 = this._$AH[n5]), o5 ||= !c3(r6) || r6 !== this._$AH[n5], r6 === T ? t4 = T : t4 !== T && (t4 += (r6 ?? "") + h3[n5 + 1]), this._$AH[n5] = r6;
- }
- o5 && !e5 && this.O(t4);
- }
- O(t4) {
- t4 === T ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, t4 ?? "");
- }
- };
- var k = class extends R {
- constructor() {
- super(...arguments), this.type = 3;
- }
- O(t4) {
- this.element[this.name] = t4 === T ? void 0 : t4;
- }
- };
- var H = class extends R {
- constructor() {
- super(...arguments), this.type = 4;
- }
- O(t4) {
- this.element.toggleAttribute(this.name, !!t4 && t4 !== T);
- }
- };
- var I = class extends R {
- constructor(t4, i4, s4, e5, h3) {
- super(t4, i4, s4, e5, h3), this.type = 5;
- }
- _$AI(t4, i4 = this) {
- if ((t4 = N(this, t4, i4, 0) ?? T) === w)
- return;
- const s4 = this._$AH, e5 = t4 === T && s4 !== T || t4.capture !== s4.capture || t4.once !== s4.once || t4.passive !== s4.passive, h3 = t4 !== T && (s4 === T || e5);
- e5 && this.element.removeEventListener(this.name, this, s4), h3 && this.element.addEventListener(this.name, this, t4), this._$AH = t4;
- }
- handleEvent(t4) {
- "function" == typeof this._$AH ? this._$AH.call(this.options?.host ?? this.element, t4) : this._$AH.handleEvent(t4);
- }
- };
- var L = class {
- constructor(t4, i4, s4) {
- this.element = t4, this.type = 6, this._$AN = void 0, this._$AM = i4, this.options = s4;
- }
- get _$AU() {
- return this._$AM._$AU;
- }
- _$AI(t4) {
- N(this, t4);
- }
- };
- var Z = t2.litHtmlPolyfillSupport;
- Z?.(V, M), (t2.litHtmlVersions ??= []).push("3.1.0");
- var j = (t4, i4, s4) => {
- const e5 = s4?.renderBefore ?? i4;
- let h3 = e5._$litPart$;
- if (void 0 === h3) {
- const t5 = s4?.renderBefore ?? null;
- e5._$litPart$ = h3 = new M(i4.insertBefore(l2(), t5), t5, void 0, s4 ?? {});
- }
- return h3._$AI(t4), h3;
- };
-
- // node_modules/lit-element/lit-element.js
- var s3 = class extends b {
- constructor() {
- super(...arguments), this.renderOptions = { host: this }, this._$Do = void 0;
- }
- createRenderRoot() {
- const t4 = super.createRenderRoot();
- return this.renderOptions.renderBefore ??= t4.firstChild, t4;
- }
- update(t4) {
- const i4 = this.render();
- this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(t4), this._$Do = j(i4, this.renderRoot, this.renderOptions);
- }
- connectedCallback() {
- super.connectedCallback(), this._$Do?.setConnected(true);
- }
- disconnectedCallback() {
- super.disconnectedCallback(), this._$Do?.setConnected(false);
- }
- render() {
- return w;
- }
- };
- s3._$litElement$ = true, s3["finalized", "finalized"] = true, globalThis.litElementHydrateSupport?.({ LitElement: s3 });
- var r4 = globalThis.litElementPolyfillSupport;
- r4?.({ LitElement: s3 });
- (globalThis.litElementVersions ??= []).push("4.0.2");
-
- // node_modules/@lit/reactive-element/decorators/custom-element.js
- var t3 = (t4) => (e5, o5) => {
- void 0 !== o5 ? o5.addInitializer(() => {
- customElements.define(t4, e5);
- }) : customElements.define(t4, e5);
- };
-
- // node_modules/@lit/reactive-element/decorators/property.js
- var o4 = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f };
- var r5 = (t4 = o4, e5, r6) => {
- const { kind: n5, metadata: i4 } = r6;
- let s4 = globalThis.litPropertyMetadata.get(i4);
- if (void 0 === s4 && globalThis.litPropertyMetadata.set(i4, s4 = /* @__PURE__ */ new Map()), s4.set(r6.name, t4), "accessor" === n5) {
- const { name: o5 } = r6;
- return { set(r7) {
- const n6 = e5.get.call(this);
- e5.set.call(this, r7), this.requestUpdate(o5, n6, t4);
- }, init(e6) {
- return void 0 !== e6 && this.C(o5, void 0, t4), e6;
- } };
- }
- if ("setter" === n5) {
- const { name: o5 } = r6;
- return function(r7) {
- const n6 = this[o5];
- e5.call(this, r7), this.requestUpdate(o5, n6, t4);
- };
- }
- throw Error("Unsupported decorator location: " + n5);
- };
- function n4(t4) {
- return (e5, o5) => "object" == typeof o5 ? r5(t4, e5, o5) : ((t5, e6, o6) => {
- const r6 = e6.hasOwnProperty(o6);
- return e6.constructor.createProperty(o6, r6 ? { ...t5, wrapped: true } : t5), r6 ? Object.getOwnPropertyDescriptor(e6, o6) : void 0;
- })(t4, e5, o5);
- }
-
- // node_modules/@posit-dev/shiny-bindings-core/dist/OptionalShiny.js
- var Shiny = window.Shiny;
-
- // node_modules/@posit-dev/shiny-bindings-core/dist/makeInputBinding.js
- function makeInputBinding(tagName, { type = null } = {}) {
- if (!Shiny) {
- return;
- }
- class NewCustomBinding extends Shiny["InputBinding"] {
- constructor() {
- super();
- }
- find(scope) {
- return $(scope).find(tagName);
- }
- getValue(el) {
- return el.value;
- }
- getType(_2) {
- return type;
- }
- subscribe(el, callback) {
- el.notifyBindingOfChange = (ad) => callback(ad ?? false);
- }
- unsubscribe(el) {
- el.notifyBindingOfChange = (_2) => {
- };
- }
- }
- Shiny.inputBindings.register(new NewCustomBinding(), `${tagName}-Binding`);
- }
-
- // srcts/index.ts
- var customInputTag = "custom-component";
- var CustomComponentEl = class extends s3 {
- constructor() {
- super(...arguments);
- this.value = 0;
- /*
- * The callback function that is called when the value of the input changes.
- * This alerts Shiny that the value has changed and it should check for the
- * latest value. This is set by the input binding.
- */
- this.notifyBindingOfChange = () => {
- };
- }
- /**
- * Function to run when the increment button is clicked.
- */
- onIncrement() {
- this.value++;
- this.notifyBindingOfChange(true);
- }
- render() {
- return x`
-
- Value: ${this.value}
-
- `;
- }
- };
- CustomComponentEl.styles = i`
- :host {
- display: block;
- border: solid 1px gray;
- padding: 16px;
- max-width: 800px;
- width: fit-content;
- }
- `;
- __decorateClass([
- n4({ type: Number })
- ], CustomComponentEl.prototype, "value", 2);
- CustomComponentEl = __decorateClass([
- t3(customInputTag)
- ], CustomComponentEl);
- makeInputBinding(customInputTag);
-})();
-/*! Bundled license information:
-
-@lit/reactive-element/css-tag.js:
- (**
- * @license
- * Copyright 2019 Google LLC
- * SPDX-License-Identifier: BSD-3-Clause
- *)
-
-@lit/reactive-element/reactive-element.js:
- (**
- * @license
- * Copyright 2017 Google LLC
- * SPDX-License-Identifier: BSD-3-Clause
- *)
-
-lit-html/lit-html.js:
- (**
- * @license
- * Copyright 2017 Google LLC
- * SPDX-License-Identifier: BSD-3-Clause
- *)
-
-lit-element/lit-element.js:
- (**
- * @license
- * Copyright 2017 Google LLC
- * SPDX-License-Identifier: BSD-3-Clause
- *)
-
-lit-html/is-server.js:
- (**
- * @license
- * Copyright 2022 Google LLC
- * SPDX-License-Identifier: BSD-3-Clause
- *)
-
-@lit/reactive-element/decorators/custom-element.js:
- (**
- * @license
- * Copyright 2017 Google LLC
- * SPDX-License-Identifier: BSD-3-Clause
- *)
-
-@lit/reactive-element/decorators/property.js:
- (**
- * @license
- * Copyright 2017 Google LLC
- * SPDX-License-Identifier: BSD-3-Clause
- *)
-
-@lit/reactive-element/decorators/state.js:
- (**
- * @license
- * Copyright 2017 Google LLC
- * SPDX-License-Identifier: BSD-3-Clause
- *)
-
-@lit/reactive-element/decorators/event-options.js:
- (**
- * @license
- * Copyright 2017 Google LLC
- * SPDX-License-Identifier: BSD-3-Clause
- *)
-
-@lit/reactive-element/decorators/base.js:
- (**
- * @license
- * Copyright 2017 Google LLC
- * SPDX-License-Identifier: BSD-3-Clause
- *)
-
-@lit/reactive-element/decorators/query.js:
- (**
- * @license
- * Copyright 2017 Google LLC
- * SPDX-License-Identifier: BSD-3-Clause
- *)
-
-@lit/reactive-element/decorators/query-all.js:
- (**
- * @license
- * Copyright 2017 Google LLC
- * SPDX-License-Identifier: BSD-3-Clause
- *)
-
-@lit/reactive-element/decorators/query-async.js:
- (**
- * @license
- * Copyright 2017 Google LLC
- * SPDX-License-Identifier: BSD-3-Clause
- *)
-
-@lit/reactive-element/decorators/query-assigned-elements.js:
- (**
- * @license
- * Copyright 2021 Google LLC
- * SPDX-License-Identifier: BSD-3-Clause
- *)
-
-@lit/reactive-element/decorators/query-assigned-nodes.js:
- (**
- * @license
- * Copyright 2017 Google LLC
- * SPDX-License-Identifier: BSD-3-Clause
- *)
-*/
diff --git a/shiny/templates/package-templates/.gitignore b/shiny/templates/package/.gitignore
similarity index 100%
rename from shiny/templates/package-templates/.gitignore
rename to shiny/templates/package/.gitignore
diff --git a/shiny/templates/package-templates/js-input/.gitignore b/shiny/templates/package/js-input/.gitignore
similarity index 100%
rename from shiny/templates/package-templates/js-input/.gitignore
rename to shiny/templates/package/js-input/.gitignore
diff --git a/shiny/templates/package-templates/js-input/README.md b/shiny/templates/package/js-input/README.md
similarity index 100%
rename from shiny/templates/package-templates/js-input/README.md
rename to shiny/templates/package/js-input/README.md
diff --git a/shiny/templates/package/js-input/_template.json b/shiny/templates/package/js-input/_template.json
new file mode 100644
index 000000000..f5bc5f1f2
--- /dev/null
+++ b/shiny/templates/package/js-input/_template.json
@@ -0,0 +1,12 @@
+{
+ "type": "package",
+ "id": "js-input",
+ "title": "Input component",
+ "description": "A Python package template providing a custom Shiny input component.",
+ "follow_up": [
+ {
+ "type": "action",
+ "text": "Learn more about custom components for Shiny at https://shiny.posit.co/py/docs/custom-components-pkg.html"
+ }
+ ]
+}
diff --git a/shiny/templates/package-templates/js-input/custom_component/__init__.py b/shiny/templates/package/js-input/custom_component/__init__.py
similarity index 100%
rename from shiny/templates/package-templates/js-input/custom_component/__init__.py
rename to shiny/templates/package/js-input/custom_component/__init__.py
diff --git a/shiny/templates/package-templates/js-input/custom_component/custom_component.py b/shiny/templates/package/js-input/custom_component/custom_component.py
similarity index 100%
rename from shiny/templates/package-templates/js-input/custom_component/custom_component.py
rename to shiny/templates/package/js-input/custom_component/custom_component.py
diff --git a/shiny/templates/package-templates/js-input/example-app/app.py b/shiny/templates/package/js-input/example-app/app.py
similarity index 100%
rename from shiny/templates/package-templates/js-input/example-app/app.py
rename to shiny/templates/package/js-input/example-app/app.py
diff --git a/shiny/templates/package-templates/js-input/package-lock.json b/shiny/templates/package/js-input/package-lock.json
similarity index 100%
rename from shiny/templates/package-templates/js-input/package-lock.json
rename to shiny/templates/package/js-input/package-lock.json
diff --git a/shiny/templates/package-templates/js-input/package.json b/shiny/templates/package/js-input/package.json
similarity index 100%
rename from shiny/templates/package-templates/js-input/package.json
rename to shiny/templates/package/js-input/package.json
diff --git a/shiny/templates/package-templates/js-input/pyproject.toml b/shiny/templates/package/js-input/pyproject.toml
similarity index 100%
rename from shiny/templates/package-templates/js-input/pyproject.toml
rename to shiny/templates/package/js-input/pyproject.toml
diff --git a/shiny/templates/package-templates/js-input/srcts/index.ts b/shiny/templates/package/js-input/srcts/index.ts
similarity index 100%
rename from shiny/templates/package-templates/js-input/srcts/index.ts
rename to shiny/templates/package/js-input/srcts/index.ts
diff --git a/shiny/templates/package-templates/js-input/tsconfig.json b/shiny/templates/package/js-input/tsconfig.json
similarity index 100%
rename from shiny/templates/package-templates/js-input/tsconfig.json
rename to shiny/templates/package/js-input/tsconfig.json
diff --git a/shiny/templates/package-templates/js-output/.gitignore b/shiny/templates/package/js-output/.gitignore
similarity index 100%
rename from shiny/templates/package-templates/js-output/.gitignore
rename to shiny/templates/package/js-output/.gitignore
diff --git a/shiny/templates/package-templates/js-output/README.md b/shiny/templates/package/js-output/README.md
similarity index 100%
rename from shiny/templates/package-templates/js-output/README.md
rename to shiny/templates/package/js-output/README.md
diff --git a/shiny/templates/package/js-output/_template.json b/shiny/templates/package/js-output/_template.json
new file mode 100644
index 000000000..440eafa5e
--- /dev/null
+++ b/shiny/templates/package/js-output/_template.json
@@ -0,0 +1,12 @@
+{
+ "type": "package",
+ "id": "js-output",
+ "title": "Output component",
+ "description": "A Python package template providing a custom Shiny output component.",
+ "follow_up": [
+ {
+ "type": "action",
+ "text": "Learn more about custom components for Shiny at https://shiny.posit.co/py/docs/custom-components-pkg.html"
+ }
+ ]
+}
diff --git a/shiny/templates/package-templates/js-output/custom_component/__init__.py b/shiny/templates/package/js-output/custom_component/__init__.py
similarity index 100%
rename from shiny/templates/package-templates/js-output/custom_component/__init__.py
rename to shiny/templates/package/js-output/custom_component/__init__.py
diff --git a/shiny/templates/package-templates/js-output/custom_component/custom_component.py b/shiny/templates/package/js-output/custom_component/custom_component.py
similarity index 100%
rename from shiny/templates/package-templates/js-output/custom_component/custom_component.py
rename to shiny/templates/package/js-output/custom_component/custom_component.py
diff --git a/shiny/templates/package-templates/js-output/example-app/app.py b/shiny/templates/package/js-output/example-app/app.py
similarity index 100%
rename from shiny/templates/package-templates/js-output/example-app/app.py
rename to shiny/templates/package/js-output/example-app/app.py
diff --git a/shiny/templates/package-templates/js-output/package-lock.json b/shiny/templates/package/js-output/package-lock.json
similarity index 100%
rename from shiny/templates/package-templates/js-output/package-lock.json
rename to shiny/templates/package/js-output/package-lock.json
diff --git a/shiny/templates/package-templates/js-output/package.json b/shiny/templates/package/js-output/package.json
similarity index 100%
rename from shiny/templates/package-templates/js-output/package.json
rename to shiny/templates/package/js-output/package.json
diff --git a/shiny/templates/package-templates/js-output/pyproject.toml b/shiny/templates/package/js-output/pyproject.toml
similarity index 100%
rename from shiny/templates/package-templates/js-output/pyproject.toml
rename to shiny/templates/package/js-output/pyproject.toml
diff --git a/shiny/templates/package-templates/js-output/srcts/index.ts b/shiny/templates/package/js-output/srcts/index.ts
similarity index 100%
rename from shiny/templates/package-templates/js-output/srcts/index.ts
rename to shiny/templates/package/js-output/srcts/index.ts
diff --git a/shiny/templates/package-templates/js-output/tsconfig.json b/shiny/templates/package/js-output/tsconfig.json
similarity index 100%
rename from shiny/templates/package-templates/js-output/tsconfig.json
rename to shiny/templates/package/js-output/tsconfig.json
diff --git a/shiny/templates/package-templates/js-react/.gitignore b/shiny/templates/package/js-react/.gitignore
similarity index 100%
rename from shiny/templates/package-templates/js-react/.gitignore
rename to shiny/templates/package/js-react/.gitignore
diff --git a/shiny/templates/package-templates/js-react/README.md b/shiny/templates/package/js-react/README.md
similarity index 100%
rename from shiny/templates/package-templates/js-react/README.md
rename to shiny/templates/package/js-react/README.md
diff --git a/shiny/templates/package/js-react/_template.json b/shiny/templates/package/js-react/_template.json
new file mode 100644
index 000000000..1b26e6ecc
--- /dev/null
+++ b/shiny/templates/package/js-react/_template.json
@@ -0,0 +1,12 @@
+{
+ "type": "package",
+ "id": "js-react",
+ "title": "React component",
+ "description": "A Python package template providing a custom React-based Shiny component.",
+ "follow_up": [
+ {
+ "type": "action",
+ "text": "Learn more about custom components for Shiny at https://shiny.posit.co/py/docs/custom-components-pkg.html"
+ }
+ ]
+}
diff --git a/shiny/templates/package-templates/js-react/custom_component/__init__.py b/shiny/templates/package/js-react/custom_component/__init__.py
similarity index 100%
rename from shiny/templates/package-templates/js-react/custom_component/__init__.py
rename to shiny/templates/package/js-react/custom_component/__init__.py
diff --git a/shiny/templates/package-templates/js-react/custom_component/custom_component.py b/shiny/templates/package/js-react/custom_component/custom_component.py
similarity index 100%
rename from shiny/templates/package-templates/js-react/custom_component/custom_component.py
rename to shiny/templates/package/js-react/custom_component/custom_component.py
diff --git a/shiny/templates/package-templates/js-react/example-app/app.py b/shiny/templates/package/js-react/example-app/app.py
similarity index 100%
rename from shiny/templates/package-templates/js-react/example-app/app.py
rename to shiny/templates/package/js-react/example-app/app.py
diff --git a/shiny/templates/package-templates/js-react/package-lock.json b/shiny/templates/package/js-react/package-lock.json
similarity index 100%
rename from shiny/templates/package-templates/js-react/package-lock.json
rename to shiny/templates/package/js-react/package-lock.json
diff --git a/shiny/templates/package-templates/js-react/package.json b/shiny/templates/package/js-react/package.json
similarity index 100%
rename from shiny/templates/package-templates/js-react/package.json
rename to shiny/templates/package/js-react/package.json
diff --git a/shiny/templates/package-templates/js-react/pyproject.toml b/shiny/templates/package/js-react/pyproject.toml
similarity index 100%
rename from shiny/templates/package-templates/js-react/pyproject.toml
rename to shiny/templates/package/js-react/pyproject.toml
diff --git a/shiny/templates/package-templates/js-react/srcts/index.tsx b/shiny/templates/package/js-react/srcts/index.tsx
similarity index 100%
rename from shiny/templates/package-templates/js-react/srcts/index.tsx
rename to shiny/templates/package/js-react/srcts/index.tsx
diff --git a/shiny/templates/package-templates/js-react/tsconfig.json b/shiny/templates/package/js-react/tsconfig.json
similarity index 100%
rename from shiny/templates/package-templates/js-react/tsconfig.json
rename to shiny/templates/package/js-react/tsconfig.json
diff --git a/tests/playwright/examples/test_shiny_create.py b/tests/playwright/examples/test_shiny_create.py
index 30f7867f4..662ae9214 100644
--- a/tests/playwright/examples/test_shiny_create.py
+++ b/tests/playwright/examples/test_shiny_create.py
@@ -8,8 +8,8 @@
from shiny._main_create import (
GithubRepoLocation,
- app_template_choices,
parse_github_arg,
+ shiny_internal_templates,
)
@@ -48,15 +48,15 @@ def subprocess_create(
@pytest.mark.flaky(reruns=reruns, reruns_delay=reruns_delay)
-@pytest.mark.parametrize("ex_app_path", get_apps("shiny/templates/app-templates"))
+@pytest.mark.parametrize("ex_app_path", get_apps("shiny/templates/app"))
def test_template_examples(page: Page, ex_app_path: str) -> None:
validate_example(page, ex_app_path)
-app_templates = list(app_template_choices.values())
-app_templates.remove("external-gallery") # Not actually a template
-app_templates.remove("js-component") # Several templates that can't be easily tested
+app_templates = [t.id for t in shiny_internal_templates.apps]
+pkg_templates = [t.id for t in shiny_internal_templates.packages]
assert len(app_templates) > 0
+assert len(pkg_templates) > 0
@pytest.mark.flaky(reruns=reruns, reruns_delay=reruns_delay)
@@ -76,7 +76,7 @@ def test_create_express(app_template: str, page: Page):
@pytest.mark.flaky(reruns=reruns, reruns_delay=reruns_delay)
-@pytest.mark.parametrize("app_template", ["js-input", "js-output", "js-react"])
+@pytest.mark.parametrize("app_template", pkg_templates)
def test_create_js(app_template: str):
with tempfile.TemporaryDirectory("example_apps") as tmpdir:
subprocess_create(app_template, dest_dir=tmpdir, package_name="my_component")
@@ -88,40 +88,40 @@ def test_parse_github_arg():
repo_owner="posit-dev",
repo_name="py-shiny",
ref="main",
- path="shiny/templates/app-templates/basic-app",
+ path="shiny/templates/app/basic-app",
)
# * {repo_owner}/{repo_name}@{ref}:{path}
actual_ref_path = parse_github_arg(
- "posit-dev/py-shiny@main:shiny/templates/app-templates/basic-app"
+ "posit-dev/py-shiny@main:shiny/templates/app/basic-app"
)
assert actual_ref_path == expected
# * {repo_owner}/{repo_name}:{path}@{ref}
actual_path_ref = parse_github_arg(
- "posit-dev/py-shiny:shiny/templates/app-templates/basic-app@main"
+ "posit-dev/py-shiny:shiny/templates/app/basic-app@main"
)
assert actual_path_ref == expected
# * {repo_owner}/{repo_name}/{path}@{ref}
actual_path_slash_ref = parse_github_arg(
- "posit-dev/py-shiny/shiny/templates/app-templates/basic-app@main"
+ "posit-dev/py-shiny/shiny/templates/app/basic-app@main"
)
assert actual_path_slash_ref == expected
# * {repo_owner}/{repo_name}/{path}?ref={ref}
actual_path_slash_query = parse_github_arg(
- "posit-dev/py-shiny/shiny/templates/app-templates/basic-app?ref=main"
+ "posit-dev/py-shiny/shiny/templates/app/basic-app?ref=main"
)
assert actual_path_slash_query == expected
actual_path_full = parse_github_arg(
- "https://github.com/posit-dev/py-shiny/tree/main/shiny/templates/app-templates/basic-app"
+ "https://github.com/posit-dev/py-shiny/tree/main/shiny/templates/app/basic-app"
)
assert actual_path_full == expected
actual_path_part = parse_github_arg(
- "github.com/posit-dev/py-shiny/tree/main/shiny/templates/app-templates/basic-app"
+ "github.com/posit-dev/py-shiny/tree/main/shiny/templates/app/basic-app"
)
assert actual_path_part == expected
@@ -130,25 +130,25 @@ def test_parse_github_arg():
# * {repo_owner}/{repo_name}:{path}
actual_path_colon = parse_github_arg(
- "posit-dev/py-shiny:shiny/templates/app-templates/basic-app"
+ "posit-dev/py-shiny:shiny/templates/app/basic-app"
)
assert actual_path_colon == expected
# * {repo_owner}/{repo_name}/{path}
actual_path_slash = parse_github_arg(
- "posit-dev/py-shiny/shiny/templates/app-templates/basic-app"
+ "posit-dev/py-shiny/shiny/templates/app/basic-app"
)
assert actual_path_slash == expected
# complicated ref
actual_ref_tag = parse_github_arg(
- "posit-dev/py-shiny@v0.1.0:shiny/templates/app-templates/basic-app"
+ "posit-dev/py-shiny@v0.1.0:shiny/templates/app/basic-app"
)
expected.ref = "v0.1.0"
assert actual_ref_tag == expected
actual_ref_branch = parse_github_arg(
- "posit-dev/py-shiny@feat/new-template:shiny/templates/app-templates/basic-app"
+ "posit-dev/py-shiny@feat/new-template:shiny/templates/app/basic-app"
)
expected.ref = "feat/new-template"
assert actual_ref_branch == expected