Jelajahi Sumber

Update base template (#2027)

Nikhil Rao 1 tahun lalu
induk
melakukan
8133aa10c9

+ 72 - 0
reflex/.templates/apps/base/README.md

@@ -0,0 +1,72 @@
+# Welcome to Reflex!
+
+This is the base Reflex template - installed when you run `reflex init`.
+
+If you want to use a different template, pass the `--template` flag to `reflex init`.
+For example, if you want a more basic starting point, you can run:
+
+```bash
+reflex init --template blank
+```
+
+## About this Template
+
+This template has the following directory structure:
+
+```bash
+.
+├── assets
+├── requirements.txt
+├── rxconfig.py
+└── {your_app}
+    ├── __init__.py
+    ├── components
+    │ └── sidebar.py
+    ├── pages
+    │ ├── __init__.py
+    │ ├── dashboard.py
+    │ ├── index.py
+    │ ├── settings.py
+    │ └── template.py
+    ├── state.py
+    ├── styles.py
+    └── {your_app}.py
+
+```
+
+See the [Project Structure docs](https://reflex.dev/docs/getting-started/project-structure/) for more information on general Reflex project structure.
+
+### Adding Pages
+
+In this template, the pages in your app are defined in `{your_app}/pages/`.
+Each page is a function that returns a Reflex component.
+For example, to edit this page you can modify `{your_app}/pages/index.py`.
+See the [pages docs](https://reflex.dev/docs/components/pages/) for more information on pages.
+
+In this template, instead of using `rx.add_page` or the `@rx.page` decorator,
+we use the `@template` decorator from `{your_app}/pages/template.py`.
+
+To add a new page:
+
+1. Add a new file in `{your_app}/pages/`. We recommend using one file per page, but you can also group pages in a single file.
+2. Add a new function with the `@template` decorator, which takes the same arguments as `@rx.page`.
+3. Import the page in your `{your_app}/{your_app}.py` file and it will automatically be added to the app.
+
+
+### Adding Components
+
+In order to keep your code organized, we recommend putting components that are
+used across multiple pages in the `{your_app}/components/` directory.
+
+In this template, we have a sidebar component in `{your_app}/components/sidebar.py`.
+
+### Adding State
+
+In this template, we define the base state of the app in `{your_app}/state.py`.
+The base state is useful for general app state that is used across multiple pages.
+
+In this template, the base state handles the toggle for the sidebar.
+
+As your app grows, we recommend using [substates](https://reflex.dev/docs/state/substates/)
+to organize your state. You can either define substates in their own files, or if the state is
+specific to a page, you can define it in the page file itself.

+ 7 - 96
reflex/.templates/apps/base/code/base.py

@@ -1,101 +1,12 @@
-"""Welcome to Reflex! This file outlines the steps to create a basic app."""
-from typing import Callable
-
-import reflex as rx
-
-from .pages import dashboard_page, home_page, settings_page
-from .sidebar import sidebar
-from .state import State
-from .styles import *
-
-meta = [
-    {
-        "name": "viewport",
-        "content": "width=device-width, shrink-to-fit=no, initial-scale=1",
-    },
-]
-
-
-def template(main_content: Callable[[], rx.Component]) -> rx.Component:
-    """The template for each page of the app.
-
-    Args:
-        main_content (Callable[[], rx.Component]): The main content of the page.
-
-    Returns:
-        rx.Component: The template for each page of the app.
-    """
-    menu_button = rx.box(
-        rx.menu(
-            rx.menu_button(
-                rx.icon(
-                    tag="hamburger",
-                    size="4em",
-                    color=text_color,
-                ),
-            ),
-            rx.menu_list(
-                rx.menu_item(rx.link("Home", href="/", width="100%")),
-                rx.menu_divider(),
-                rx.menu_item(
-                    rx.link("About", href="https://github.com/reflex-dev", width="100%")
-                ),
-                rx.menu_item(
-                    rx.link("Contact", href="mailto:founders@=reflex.dev", width="100%")
-                ),
-            ),
-        ),
-        position="fixed",
-        right="1.5em",
-        top="1.5em",
-        z_index="500",
-    )
-
-    return rx.hstack(
-        sidebar(),
-        main_content(),
-        rx.spacer(),
-        menu_button,
-        align_items="flex-start",
-        transition="left 0.5s, width 0.5s",
-        position="relative",
-        left=rx.cond(State.sidebar_displayed, "0px", f"-{sidebar_width}"),
-    )
+"""Welcome to Reflex!."""
 
+from code import styles
 
-@rx.page("/", meta=meta)
-@template
-def home() -> rx.Component:
-    """Home page.
-
-    Returns:
-        rx.Component: The home page.
-    """
-    return home_page()
-
-
-@rx.page("/settings", meta=meta)
-@template
-def settings() -> rx.Component:
-    """Settings page.
-
-    Returns:
-        rx.Component: The settings page.
-    """
-    return settings_page()
-
-
-@rx.page("/dashboard", meta=meta)
-@template
-def dashboard() -> rx.Component:
-    """Dashboard page.
-
-    Returns:
-        rx.Component: The dashboard page.
-    """
-    return dashboard_page()
+# Import all the pages.
+from code.pages import *
 
+import reflex as rx
 
-# Add state and page to the app.
-app = rx.App(style=base_style)
+# Create the app and compile it.
+app = rx.App(style=styles.base_style)
 app.compile()

+ 0 - 0
reflex/.templates/apps/default/code/__init__.py → reflex/.templates/apps/base/code/components/__init__.py


+ 45 - 37
reflex/.templates/apps/base/code/sidebar.py → reflex/.templates/apps/base/code/components/sidebar.py

@@ -1,23 +1,25 @@
 """Sidebar component for the app."""
 
-import reflex as rx
+from code import styles
+from code.state import State
 
-from .state import State
-from .styles import *
+import reflex as rx
 
 
 def sidebar_header() -> rx.Component:
     """Sidebar header.
 
     Returns:
-        rx.Component: The sidebar header component.
+        The sidebar header component.
     """
     return rx.hstack(
+        # The logo.
         rx.image(
             src="/icon.svg",
             height="2em",
         ),
         rx.spacer(),
+        # Link to Reflex GitHub repo.
         rx.link(
             rx.center(
                 rx.image(
@@ -25,17 +27,17 @@ def sidebar_header() -> rx.Component:
                     height="3em",
                     padding="0.5em",
                 ),
-                box_shadow=box_shadow,
+                box_shadow=styles.box_shadow,
                 bg="transparent",
-                border_radius=border_radius,
+                border_radius=styles.border_radius,
                 _hover={
-                    "bg": accent_color,
+                    "bg": styles.accent_color,
                 },
             ),
             href="https://github.com/reflex-dev/reflex",
         ),
         width="100%",
-        border_bottom=border,
+        border_bottom=styles.border,
         padding="1em",
     )
 
@@ -44,7 +46,7 @@ def sidebar_footer() -> rx.Component:
     """Sidebar footer.
 
     Returns:
-        rx.Component: The sidebar footer component.
+        The sidebar footer component.
     """
     return rx.hstack(
         rx.link(
@@ -55,15 +57,15 @@ def sidebar_footer() -> rx.Component:
                     padding="0.5em",
                 ),
                 bg="transparent",
-                border_radius=border_radius,
-                **hover_accent_bg,
+                border_radius=styles.border_radius,
+                **styles.hover_accent_bg,
             ),
             on_click=State.toggle_sidebar_displayed,
             transform=rx.cond(~State.sidebar_displayed, "rotate(180deg)", ""),
             transition="transform 0.5s, left 0.5s",
             position="relative",
             left=rx.cond(State.sidebar_displayed, "0px", "20.5em"),
-            **overlapping_button_style,
+            **styles.overlapping_button_style,
         ),
         rx.spacer(),
         rx.link(
@@ -79,7 +81,7 @@ def sidebar_footer() -> rx.Component:
             href="https://reflex.dev/blog/",
         ),
         width="100%",
-        border_top=border,
+        border_top=styles.border,
         padding="1em",
     )
 
@@ -88,13 +90,18 @@ def sidebar_item(text: str, icon: str, url: str) -> rx.Component:
     """Sidebar item.
 
     Args:
-        text (str): The text of the item.
-        icon (str): The icon of the item.
-        url (str): The URL of the item.
+        text: The text of the item.
+        icon: The icon of the item.
+        url: The URL of the item.
 
     Returns:
         rx.Component: The sidebar item component.
     """
+    # Whether the item is active.
+    active = (State.router.page.path == f"/{text.lower()}") | (
+        (State.router.page.path == "/") & text == "Home"
+    )
+
     return rx.link(
         rx.hstack(
             rx.image(
@@ -106,17 +113,17 @@ def sidebar_item(text: str, icon: str, url: str) -> rx.Component:
                 text,
             ),
             bg=rx.cond(
-                State.origin_url == f"/{text.lower()}/",
-                accent_color,
+                active,
+                styles.accent_color,
                 "transparent",
             ),
             color=rx.cond(
-                State.origin_url == f"/{text.lower()}/",
-                accent_text_color,
-                text_color,
+                active,
+                styles.accent_text_color,
+                styles.text_color,
             ),
-            border_radius=border_radius,
-            box_shadow=box_shadow,
+            border_radius=styles.border_radius,
+            box_shadow=styles.box_shadow,
             width="100%",
             padding_x="1em",
         ),
@@ -126,25 +133,26 @@ def sidebar_item(text: str, icon: str, url: str) -> rx.Component:
 
 
 def sidebar() -> rx.Component:
-    """Sidebar.
+    """The sidebar.
 
     Returns:
-        rx.Component: The sidebar component.
+        The sidebar component.
     """
+    # Get all the decorated pages and add them to the sidebar.
+    from reflex.page import get_decorated_pages
+
     return rx.box(
         rx.vstack(
             sidebar_header(),
             rx.vstack(
-                sidebar_item(
-                    "Dashboard",
-                    "/github.svg",
-                    "/dashboard",
-                ),
-                sidebar_item(
-                    "Settings",
-                    "/github.svg",
-                    "/settings",
-                ),
+                *[
+                    sidebar_item(
+                        text=page.get("title", page["route"].strip("/").capitalize()),
+                        icon=page.get("image", "/github.svg"),
+                        url=page["route"],
+                    )
+                    for page in get_decorated_pages()
+                ],
                 width="100%",
                 overflow_y="auto",
                 align_items="flex-start",
@@ -154,9 +162,9 @@ def sidebar() -> rx.Component:
             sidebar_footer(),
             height="100dvh",
         ),
-        min_width=sidebar_width,
+        min_width=styles.sidebar_width,
         height="100%",
         position="sticky",
         top="0px",
-        border_right=border,
+        border_right=styles.border,
     )

+ 3 - 4
reflex/.templates/apps/base/code/pages/__init__.py

@@ -1,4 +1,3 @@
-"""The pages of the app."""
-from .dashboard import dashboard_page
-from .home import home_page
-from .settings import settings_page
+from .dashboard import dashboard
+from .index import index
+from .settings import settings

+ 13 - 20
reflex/.templates/apps/base/code/pages/dashboard.py

@@ -1,28 +1,21 @@
-"""The dashboard page for the template."""
-import reflex as rx
+"""The dashboard page."""
+from code.templates import template
 
-from ..styles import *
+import reflex as rx
 
 
-def dashboard_page() -> rx.Component:
-    """The UI for the dashboard page.
+@template(route="/dashboard", title="Dashboard")
+def dashboard() -> rx.Component:
+    """The dashboard page.
 
     Returns:
-        rx.Component: The UI for the dashboard page.
+        The UI for the dashboard page.
     """
-    return rx.box(
-        rx.vstack(
-            rx.heading(
-                "Dashboard",
-                font_size="3em",
-            ),
-            rx.text(
-                "Welcome to Reflex!",
-            ),
-            rx.text(
-                "You can use this template to get started with Reflex.",
-            ),
-            style=template_content_style,
+    return rx.vstack(
+        rx.heading("Dashboard", font_size="3em"),
+        rx.text("Welcome to Reflex!"),
+        rx.text(
+            "You can edit this page in ",
+            rx.code("{your_app}/pages/dashboard.py"),
         ),
-        style=template_page_style,
     )

+ 0 - 28
reflex/.templates/apps/base/code/pages/home.py

@@ -1,28 +0,0 @@
-"""The home page of the app."""
-import reflex as rx
-
-from ..styles import *
-
-
-def home_page() -> rx.Component:
-    """The UI for the home page.
-
-    Returns:
-        rx.Component: The UI for the home page.
-    """
-    return rx.box(
-        rx.vstack(
-            rx.heading(
-                "Home",
-                font_size="3em",
-            ),
-            rx.text(
-                "Welcome to Reflex!",
-            ),
-            rx.text(
-                "You can use this template to get started with Reflex.",
-            ),
-            style=template_content_style,
-        ),
-        style=template_page_style,
-    )

+ 18 - 0
reflex/.templates/apps/base/code/pages/index.py

@@ -0,0 +1,18 @@
+"""The home page of the app."""
+
+from code import styles
+from code.templates import template
+
+import reflex as rx
+
+
+@template(route="/", title="Home", image="/logo.svg")
+def index() -> rx.Component:
+    """The home page.
+
+    Returns:
+        The UI for the home page.
+    """
+    with open("README.md") as readme:
+        content = readme.read()
+    return rx.markdown(content, component_map=styles.markdown_style)

+ 14 - 20
reflex/.templates/apps/base/code/pages/settings.py

@@ -1,28 +1,22 @@
-"""The settings page for the template."""
-import reflex as rx
+"""The settings page."""
+
+from code.templates import template
 
-from ..styles import *
+import reflex as rx
 
 
-def settings_page() -> rx.Component:
-    """The UI for the settings page.
+@template(route="/settings", title="Settings")
+def settings() -> rx.Component:
+    """The settings page.
 
     Returns:
-        rx.Component: The UI for the settings page.
+        The UI for the settings page.
     """
-    return rx.box(
-        rx.vstack(
-            rx.heading(
-                "Settings",
-                font_size="3em",
-            ),
-            rx.text(
-                "Welcome to Reflex!",
-            ),
-            rx.text(
-                "You can use this template to get started with Reflex.",
-            ),
-            style=template_content_style,
+    return rx.vstack(
+        rx.heading("Settings", font_size="3em"),
+        rx.text("Welcome to Reflex!"),
+        rx.text(
+            "You can edit this page in ",
+            rx.code("{your_app}/pages/settings.py"),
         ),
-        style=template_page_style,
     )

+ 1 - 9
reflex/.templates/apps/base/code/state.py

@@ -6,17 +6,9 @@ import reflex as rx
 class State(rx.State):
     """State for the app."""
 
+    # Whether the sidebar is displayed.
     sidebar_displayed: bool = True
 
-    @rx.var
-    def origin_url(self) -> str:
-        """Get the url of the current page.
-
-        Returns:
-            str: The url of the current page.
-        """
-        return self.router_data.get("asPath", "")
-
     def toggle_sidebar_displayed(self) -> None:
         """Toggle the sidebar displayed."""
         self.sidebar_displayed = not self.sidebar_displayed

+ 19 - 2
reflex/.templates/apps/base/code/styles.py

@@ -1,7 +1,7 @@
 """Styles for the app."""
-import reflex as rx
+from code.state import State
 
-from .state import State
+import reflex as rx
 
 border_radius = "0.375rem"
 box_shadow = "0px 0px 0px 1px rgba(84, 82, 95, 0.14)"
@@ -53,3 +53,20 @@ base_style = {
     },
     rx.MenuItem: hover_accent_bg,
 }
+
+markdown_style = {
+    "code": lambda text: rx.code(text, color="#1F1944", bg="#EAE4FD"),
+    "a": lambda text, **props: rx.link(
+        text,
+        **props,
+        font_weight="bold",
+        color="#03030B",
+        text_decoration="underline",
+        text_decoration_color="#AD9BF8",
+        _hover={
+            "color": "#AD9BF8",
+            "text_decoration": "underline",
+            "text_decoration_color": "#03030B",
+        },
+    ),
+}

+ 1 - 0
reflex/.templates/apps/base/code/templates/__init__.py

@@ -0,0 +1 @@
+from .template import template

+ 99 - 0
reflex/.templates/apps/base/code/templates/template.py

@@ -0,0 +1,99 @@
+"""Common templates used between pages in the app."""
+
+from code import styles
+from code.components.sidebar import sidebar
+from code.state import State
+from typing import Callable
+
+import reflex as rx
+
+# Meta tags for the app.
+default_meta = [
+    {
+        "name": "viewport",
+        "content": "width=device-width, shrink-to-fit=no, initial-scale=1",
+    },
+]
+
+
+def menu_button() -> rx.Component:
+    """The menu button on the top right of the page.
+
+    Returns:
+        The menu button component.
+    """
+    return rx.box(
+        rx.menu(
+            rx.menu_button(
+                rx.icon(
+                    tag="hamburger",
+                    size="4em",
+                    color=styles.text_color,
+                ),
+            ),
+            rx.menu_list(
+                rx.menu_item(rx.link("Home", href="/", width="100%")),
+                rx.menu_divider(),
+                rx.menu_item(
+                    rx.link("About", href="https://github.com/reflex-dev", width="100%")
+                ),
+                rx.menu_item(
+                    rx.link("Contact", href="mailto:founders@=reflex.dev", width="100%")
+                ),
+            ),
+        ),
+        position="fixed",
+        right="1.5em",
+        top="1.5em",
+        z_index="500",
+    )
+
+
+def template(
+    **page_kwargs: dict,
+) -> Callable[[Callable[[], rx.Component]], rx.Component]:
+    """The template for each page of the app.
+
+    Args:
+        page_kwargs: Keyword arguments to pass to the page.
+
+    Returns:
+        The template with the page content.
+    """
+
+    def decorator(page_content: Callable[[], rx.Component]) -> rx.Component:
+        """The template for each page of the app.
+
+        Args:
+            page_content: The content of the page.
+
+        Returns:
+            The template with the page content.
+        """
+        # Get the meta tags for the page.
+        page_kwargs["meta"] = [*default_meta, *page_kwargs.get("meta", [])]
+
+        @rx.page(**page_kwargs)
+        def templated_page():
+            return rx.hstack(
+                sidebar(),
+                rx.box(
+                    rx.box(
+                        page_content(),
+                        **styles.template_content_style,
+                    ),
+                    **styles.template_page_style,
+                ),
+                rx.spacer(),
+                menu_button(),
+                align_items="flex-start",
+                transition="left 0.5s, width 0.5s",
+                position="relative",
+                left=rx.cond(
+                    State.sidebar_displayed, "0px", f"-{styles.sidebar_width}"
+                ),
+            )
+
+        return templated_page
+
+    return decorator

+ 0 - 0
reflex/.templates/apps/default/assets/favicon.ico → reflex/.templates/apps/blank/assets/favicon.ico


+ 0 - 0
reflex/.templates/apps/blank/code/__init__.py


+ 0 - 0
reflex/.templates/apps/default/code/default.py → reflex/.templates/apps/blank/code/blank.py


+ 1 - 1
reflex/components/typography/markdown.py

@@ -62,7 +62,7 @@ def get_base_component_map() -> dict[str, Callable]:
         "p": lambda value: Text.create(value, margin_y="1em"),
         "ul": lambda value: UnorderedList.create(value, margin_y="1em"),  # type: ignore
         "ol": lambda value: OrderedList.create(value, margin_y="1em"),  # type: ignore
-        "li": lambda value: ListItem.create(value),
+        "li": lambda value: ListItem.create(value, margin_y="0.5em"),
         "a": lambda value: Link.create(value),
         "code": lambda value: Code.create(value),
         "codeblock": lambda *_, **props: CodeBlock.create(

+ 2 - 0
reflex/constants/base.py

@@ -89,6 +89,8 @@ class Templates(SimpleNamespace):
         WEB_TEMPLATE = os.path.join(BASE, "web")
         # The jinja template directory.
         JINJA_TEMPLATE = os.path.join(BASE, "jinja")
+        # Where the code for the templates is stored.
+        CODE = "code"
 
 
 class Next(SimpleNamespace):

+ 12 - 0
reflex/page.py

@@ -61,3 +61,15 @@ def page(
         return render_fn
 
     return decorator
+
+
+def get_decorated_pages() -> list[dict]:
+    """Get the decorated pages.
+
+    Returns:
+        The decorated pages.
+    """
+    return sorted(
+        [page_data for render_fn, page_data in DECORATED_PAGES],
+        key=lambda x: x["route"],
+    )

+ 1 - 1
reflex/reflex.py

@@ -71,7 +71,7 @@ def init(
         None, metavar="APP_NAME", help="The name of the app to initialize."
     ),
     template: constants.Templates.Kind = typer.Option(
-        constants.Templates.Kind.DEFAULT.value,
+        constants.Templates.Kind.BASE.value,
         help="The template to initialize the app with.",
     ),
     loglevel: constants.LogLevel = typer.Option(

+ 1 - 1
reflex/testing.py

@@ -154,7 +154,7 @@ class AppHarness:
             with chdir(self.app_path):
                 reflex.reflex.init(
                     name=self.app_name,
-                    template=reflex.constants.Templates.Kind.DEFAULT,
+                    template=reflex.constants.Templates.Kind.BLANK,
                     loglevel=reflex.constants.LogLevel.INFO,
                 )
                 self.app_module_path.write_text(source_code)

+ 1 - 1
reflex/utils/exec.py

@@ -34,7 +34,7 @@ def detect_package_change(json_file_path: str) -> str:
     """Calculates the SHA-256 hash of a JSON file and returns it as a hexadecimal string.
 
     Args:
-        json_file_path (str): The path to the JSON file to be hashed.
+        json_file_path: The path to the JSON file to be hashed.
 
     Returns:
         str: The SHA-256 hash of the JSON file as a hexadecimal string.

+ 19 - 0
reflex/utils/path_ops.py

@@ -4,6 +4,7 @@ from __future__ import annotations
 
 import json
 import os
+import re
 import shutil
 from pathlib import Path
 
@@ -173,3 +174,21 @@ def update_json_file(file_path: str, update_dict: dict[str, int | str]):
     # Write the updated json object to the file
     with open(fp, "w") as f:
         json.dump(json_object, f, ensure_ascii=False)
+
+
+def find_replace(directory: str, find: str, replace: str):
+    """Recursively find and replace text in files in a directory.
+
+    Args:
+        directory: The directory to search.
+        find: The text to find.
+        replace: The text to replace.
+    """
+    for root, _dirs, files in os.walk(directory):
+        for file in files:
+            filepath = os.path.join(root, file)
+            with open(filepath, "r", encoding="utf-8") as f:
+                text = f.read()
+            text = re.sub(find, replace, text)
+            with open(filepath, "w") as f:
+                f.write(text)

+ 16 - 8
reflex/utils/prerequisites.py

@@ -249,17 +249,25 @@ def initialize_app_directory(app_name: str, template: constants.Templates.Kind):
         template: The template to use.
     """
     console.log("Initializing the app directory.")
-    path_ops.cp(
-        os.path.join(constants.Templates.Dirs.BASE, "apps", template.value, "code"),
-        app_name,
-    )
+
+    # Copy the template to the current directory.
+    template_dir = os.path.join(constants.Templates.Dirs.BASE, "apps", template.value)
+    for file in os.listdir(template_dir):
+        # Copy the file but keep the name the same.
+        path_ops.cp(os.path.join(template_dir, file), file)
+
+    # Rename the template app to the app name.
+    path_ops.mv(constants.Templates.Dirs.CODE, app_name)
     path_ops.mv(
-        os.path.join(app_name, template.value + ".py"),
+        os.path.join(app_name, template.value + constants.Ext.PY),
         os.path.join(app_name, app_name + constants.Ext.PY),
     )
-    path_ops.cp(
-        os.path.join(constants.Templates.Dirs.BASE, "apps", template.value, "assets"),
-        constants.Dirs.APP_ASSETS,
+
+    # Fix up the imports.
+    path_ops.find_replace(
+        app_name,
+        f"from {constants.Templates.Dirs.CODE}",
+        f"from {app_name}",
     )