瀏覽代碼

Add a sticky Built with Reflex badge (#4584)

* add the watermark class

* remove shortcut exposing badge publicly for now

* Rename as "sticky" because "watermark" has a negative connotation

* Add config `show_built_with_reflex`

This config option is available for authenticated users on various plan tiers

* py3.11 compatible f-string

* sticky badge inherit from A instead of using on_click/redirect

* fix integration test

* Move export checking logic to reflex CLI

* rx.logo: make it accessible to screen readers

Add role="img" aria_label="Reflex" and title="Reflex".

* Hide the built with reflex badge for localhost

* Revert "fix integration test"

This reverts commit a978684d70b59a077b714792603bcefd1939b41a.

* experimental: do not show warning for internal imports

Only show experimental feature warnings when accessing the names through the
rx._x namespace.

If reflex internally imports the names via deep imports, then this bypasses the
warning to avoid showing it to users that have no control over how the
framework uses experimental features.

* add help link for show_built_with_reflex option

* pre-commit fixes

---------

Co-authored-by: Masen Furer <m_github@0x26.net>
Thomas Brandého 3 月之前
父節點
當前提交
ef93161840

+ 12 - 0
reflex/app.py

@@ -70,6 +70,7 @@ from reflex.components.core.client_side_routing import (
     Default404Page,
     wait_for_client_redirect,
 )
+from reflex.components.core.sticky import sticky
 from reflex.components.core.upload import Upload, get_upload_dir
 from reflex.components.radix import themes
 from reflex.config import environment, get_config
@@ -875,6 +876,15 @@ class App(MiddlewareMixin, LifespanMixin):
                 continue
             self._pages[k] = self._add_error_boundary_to_component(component)
 
+    def _setup_sticky_badge(self):
+        """Add the sticky badge to the app."""
+        for k, component in self._pages.items():
+            # Would be nice to share single sticky_badge across all pages, but
+            # it bungles the StatefulComponent compile step.
+            sticky_badge = sticky()
+            sticky_badge._add_style_recursive({})
+            self._pages[k] = Fragment.create(sticky_badge, component)
+
     def _apply_decorated_pages(self):
         """Add @rx.page decorated pages to the app.
 
@@ -1005,6 +1015,8 @@ class App(MiddlewareMixin, LifespanMixin):
         self._validate_var_dependencies()
         self._setup_overlay_component()
         self._setup_error_boundary()
+        if config.show_built_with_reflex:
+            self._setup_sticky_badge()
 
         progress.advance(task)
 

+ 160 - 0
reflex/components/core/sticky.py

@@ -0,0 +1,160 @@
+"""Components for displaying the Reflex sticky logo."""
+
+from reflex.components.component import ComponentNamespace
+from reflex.components.core.colors import color
+from reflex.components.core.cond import color_mode_cond, cond
+from reflex.components.core.responsive import tablet_and_desktop
+from reflex.components.el.elements.inline import A
+from reflex.components.el.elements.media import Path, Rect, Svg
+from reflex.components.radix.themes.typography.text import Text
+from reflex.experimental.client_state import ClientStateVar
+from reflex.style import Style
+from reflex.vars.base import Var, VarData
+
+
+class StickyLogo(Svg):
+    """A simple Reflex logo SVG with only the letter R."""
+
+    @classmethod
+    def create(cls):
+        """Create the simple Reflex logo SVG.
+
+        Returns:
+            The simple Reflex logo SVG.
+        """
+        return super().create(
+            Rect.create(width="16", height="16", rx="2", fill="#6E56CF"),
+            Path.create(d="M10 9V13H12V9H10Z", fill="white"),
+            Path.create(d="M4 3V13H6V9H10V7H6V5H10V7H12V3H4Z", fill="white"),
+            width="16",
+            height="16",
+            viewBox="0 0 16 16",
+            xmlns="http://www.w3.org/2000/svg",
+        )
+
+    def add_style(self):
+        """Add the style to the component.
+
+        Returns:
+            The style of the component.
+        """
+        return Style(
+            {
+                "fill": "white",
+            }
+        )
+
+
+class StickyLabel(Text):
+    """A label that displays the Reflex sticky."""
+
+    @classmethod
+    def create(cls):
+        """Create the sticky label.
+
+        Returns:
+            The sticky label.
+        """
+        return super().create("Built with Reflex")
+
+    def add_style(self):
+        """Add the style to the component.
+
+        Returns:
+            The style of the component.
+        """
+        return Style(
+            {
+                "color": color("slate", 1),
+                "font_weight": "600",
+                "font_family": "'Instrument Sans', sans-serif",
+                "font_size": "0.875rem",
+                "line_height": "1rem",
+                "letter_spacing": "-0.00656rem",
+            }
+        )
+
+
+class StickyBadge(A):
+    """A badge that displays the Reflex sticky logo."""
+
+    @classmethod
+    def create(cls):
+        """Create the sticky badge.
+
+        Returns:
+            The sticky badge.
+        """
+        return super().create(
+            StickyLogo.create(),
+            tablet_and_desktop(StickyLabel.create()),
+            href="https://reflex.dev",
+            target="_blank",
+            width="auto",
+            padding="0.375rem",
+            align="center",
+            text_align="center",
+        )
+
+    def add_style(self):
+        """Add the style to the component.
+
+        Returns:
+            The style of the component.
+        """
+        is_localhost_cs = ClientStateVar.create(
+            "is_localhost",
+            default=True,
+            global_ref=False,
+        )
+        localhost_hostnames = Var.create(
+            ["localhost", "127.0.0.1", "[::1]"]
+        ).guess_type()
+        is_localhost_expr = localhost_hostnames.contains(
+            Var("window.location.hostname", _var_type=str).guess_type(),
+        )
+        check_is_localhost = Var(
+            f"useEffect(({is_localhost_cs}) => {is_localhost_cs.set}({is_localhost_expr}), [])",
+            _var_data=VarData(
+                imports={"react": "useEffect"},
+            ),
+        )
+        is_localhost = is_localhost_cs.value._replace(
+            merge_var_data=VarData.merge(
+                check_is_localhost._get_all_var_data(),
+                VarData(hooks={str(check_is_localhost): None}),
+            ),
+        )
+        return Style(
+            {
+                "position": "fixed",
+                "bottom": "1rem",
+                "right": "1rem",
+                # Do not show the badge on localhost.
+                "display": cond(is_localhost, "none", "flex"),
+                "flex-direction": "row",
+                "gap": "0.375rem",
+                "align-items": "center",
+                "width": "auto",
+                "border-radius": "0.5rem",
+                "color": color_mode_cond("#E5E7EB", "#27282B"),
+                "border": color_mode_cond("1px solid #27282B", "1px solid #E5E7EB"),
+                "background-color": color_mode_cond("#151618", "#FCFCFD"),
+                "padding": "0.375rem",
+                "transition": "background-color 0.2s ease-in-out",
+                "box-shadow": "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
+                "z-index": "9998",
+                "cursor": "pointer",
+            },
+        )
+
+
+class StickyNamespace(ComponentNamespace):
+    """Sticky components namespace."""
+
+    __call__ = staticmethod(StickyBadge.create)
+    label = staticmethod(StickyLabel.create)
+    logo = staticmethod(StickyLogo.create)
+
+
+sticky = StickyNamespace()

+ 449 - 0
reflex/components/core/sticky.pyi

@@ -0,0 +1,449 @@
+"""Stub file for reflex/components/core/sticky.py"""
+
+# ------------------- DO NOT EDIT ----------------------
+# This file was generated by `reflex/utils/pyi_generator.py`!
+# ------------------------------------------------------
+from typing import Any, Dict, Literal, Optional, Union, overload
+
+from reflex.components.component import ComponentNamespace
+from reflex.components.core.breakpoints import Breakpoints
+from reflex.components.el.elements.inline import A
+from reflex.components.el.elements.media import Svg
+from reflex.components.radix.themes.typography.text import Text
+from reflex.event import BASE_STATE, EventType
+from reflex.style import Style
+from reflex.vars.base import Var
+
+class StickyLogo(Svg):
+    @overload
+    @classmethod
+    def create(  # type: ignore
+        cls,
+        *children,
+        width: Optional[Union[Var[Union[int, str]], int, str]] = None,
+        height: Optional[Union[Var[Union[int, str]], int, str]] = None,
+        xmlns: Optional[Union[Var[str], str]] = None,
+        access_key: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        auto_capitalize: Optional[
+            Union[Var[Union[bool, int, str]], bool, int, str]
+        ] = None,
+        content_editable: Optional[
+            Union[Var[Union[bool, int, str]], bool, int, str]
+        ] = None,
+        context_menu: Optional[
+            Union[Var[Union[bool, int, str]], bool, int, str]
+        ] = None,
+        dir: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        draggable: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        enter_key_hint: Optional[
+            Union[Var[Union[bool, int, str]], bool, int, str]
+        ] = None,
+        hidden: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        input_mode: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        item_prop: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        lang: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        role: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        slot: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        spell_check: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        tab_index: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        title: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        style: Optional[Style] = None,
+        key: Optional[Any] = None,
+        id: Optional[Any] = None,
+        class_name: Optional[Any] = None,
+        autofocus: Optional[bool] = None,
+        custom_attrs: Optional[Dict[str, Union[Var, Any]]] = None,
+        on_blur: Optional[EventType[[], BASE_STATE]] = None,
+        on_click: Optional[EventType[[], BASE_STATE]] = None,
+        on_context_menu: Optional[EventType[[], BASE_STATE]] = None,
+        on_double_click: Optional[EventType[[], BASE_STATE]] = None,
+        on_focus: Optional[EventType[[], BASE_STATE]] = None,
+        on_mount: Optional[EventType[[], BASE_STATE]] = None,
+        on_mouse_down: Optional[EventType[[], BASE_STATE]] = None,
+        on_mouse_enter: Optional[EventType[[], BASE_STATE]] = None,
+        on_mouse_leave: Optional[EventType[[], BASE_STATE]] = None,
+        on_mouse_move: Optional[EventType[[], BASE_STATE]] = None,
+        on_mouse_out: Optional[EventType[[], BASE_STATE]] = None,
+        on_mouse_over: Optional[EventType[[], BASE_STATE]] = None,
+        on_mouse_up: Optional[EventType[[], BASE_STATE]] = None,
+        on_scroll: Optional[EventType[[], BASE_STATE]] = None,
+        on_unmount: Optional[EventType[[], BASE_STATE]] = None,
+        **props,
+    ) -> "StickyLogo":
+        """Create the simple Reflex logo SVG.
+
+        Returns:
+            The simple Reflex logo SVG.
+        """
+        ...
+
+    def add_style(self): ...
+
+class StickyLabel(Text):
+    @overload
+    @classmethod
+    def create(  # type: ignore
+        cls,
+        *children,
+        as_child: Optional[Union[Var[bool], bool]] = None,
+        as_: Optional[
+            Union[
+                Literal[
+                    "abbr",
+                    "b",
+                    "cite",
+                    "del",
+                    "div",
+                    "em",
+                    "i",
+                    "ins",
+                    "kbd",
+                    "label",
+                    "mark",
+                    "p",
+                    "s",
+                    "samp",
+                    "span",
+                    "sub",
+                    "sup",
+                    "u",
+                ],
+                Var[
+                    Literal[
+                        "abbr",
+                        "b",
+                        "cite",
+                        "del",
+                        "div",
+                        "em",
+                        "i",
+                        "ins",
+                        "kbd",
+                        "label",
+                        "mark",
+                        "p",
+                        "s",
+                        "samp",
+                        "span",
+                        "sub",
+                        "sup",
+                        "u",
+                    ]
+                ],
+            ]
+        ] = None,
+        size: Optional[
+            Union[
+                Breakpoints[str, Literal["1", "2", "3", "4", "5", "6", "7", "8", "9"]],
+                Literal["1", "2", "3", "4", "5", "6", "7", "8", "9"],
+                Var[
+                    Union[
+                        Breakpoints[
+                            str, Literal["1", "2", "3", "4", "5", "6", "7", "8", "9"]
+                        ],
+                        Literal["1", "2", "3", "4", "5", "6", "7", "8", "9"],
+                    ]
+                ],
+            ]
+        ] = None,
+        weight: Optional[
+            Union[
+                Breakpoints[str, Literal["bold", "light", "medium", "regular"]],
+                Literal["bold", "light", "medium", "regular"],
+                Var[
+                    Union[
+                        Breakpoints[str, Literal["bold", "light", "medium", "regular"]],
+                        Literal["bold", "light", "medium", "regular"],
+                    ]
+                ],
+            ]
+        ] = None,
+        align: Optional[
+            Union[
+                Breakpoints[str, Literal["center", "left", "right"]],
+                Literal["center", "left", "right"],
+                Var[
+                    Union[
+                        Breakpoints[str, Literal["center", "left", "right"]],
+                        Literal["center", "left", "right"],
+                    ]
+                ],
+            ]
+        ] = None,
+        trim: Optional[
+            Union[
+                Breakpoints[str, Literal["both", "end", "normal", "start"]],
+                Literal["both", "end", "normal", "start"],
+                Var[
+                    Union[
+                        Breakpoints[str, Literal["both", "end", "normal", "start"]],
+                        Literal["both", "end", "normal", "start"],
+                    ]
+                ],
+            ]
+        ] = None,
+        color_scheme: Optional[
+            Union[
+                Literal[
+                    "amber",
+                    "blue",
+                    "bronze",
+                    "brown",
+                    "crimson",
+                    "cyan",
+                    "gold",
+                    "grass",
+                    "gray",
+                    "green",
+                    "indigo",
+                    "iris",
+                    "jade",
+                    "lime",
+                    "mint",
+                    "orange",
+                    "pink",
+                    "plum",
+                    "purple",
+                    "red",
+                    "ruby",
+                    "sky",
+                    "teal",
+                    "tomato",
+                    "violet",
+                    "yellow",
+                ],
+                Var[
+                    Literal[
+                        "amber",
+                        "blue",
+                        "bronze",
+                        "brown",
+                        "crimson",
+                        "cyan",
+                        "gold",
+                        "grass",
+                        "gray",
+                        "green",
+                        "indigo",
+                        "iris",
+                        "jade",
+                        "lime",
+                        "mint",
+                        "orange",
+                        "pink",
+                        "plum",
+                        "purple",
+                        "red",
+                        "ruby",
+                        "sky",
+                        "teal",
+                        "tomato",
+                        "violet",
+                        "yellow",
+                    ]
+                ],
+            ]
+        ] = None,
+        high_contrast: Optional[Union[Var[bool], bool]] = None,
+        access_key: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        auto_capitalize: Optional[
+            Union[Var[Union[bool, int, str]], bool, int, str]
+        ] = None,
+        content_editable: Optional[
+            Union[Var[Union[bool, int, str]], bool, int, str]
+        ] = None,
+        context_menu: Optional[
+            Union[Var[Union[bool, int, str]], bool, int, str]
+        ] = None,
+        dir: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        draggable: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        enter_key_hint: Optional[
+            Union[Var[Union[bool, int, str]], bool, int, str]
+        ] = None,
+        hidden: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        input_mode: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        item_prop: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        lang: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        role: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        slot: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        spell_check: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        tab_index: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        title: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        style: Optional[Style] = None,
+        key: Optional[Any] = None,
+        id: Optional[Any] = None,
+        class_name: Optional[Any] = None,
+        autofocus: Optional[bool] = None,
+        custom_attrs: Optional[Dict[str, Union[Var, Any]]] = None,
+        on_blur: Optional[EventType[[], BASE_STATE]] = None,
+        on_click: Optional[EventType[[], BASE_STATE]] = None,
+        on_context_menu: Optional[EventType[[], BASE_STATE]] = None,
+        on_double_click: Optional[EventType[[], BASE_STATE]] = None,
+        on_focus: Optional[EventType[[], BASE_STATE]] = None,
+        on_mount: Optional[EventType[[], BASE_STATE]] = None,
+        on_mouse_down: Optional[EventType[[], BASE_STATE]] = None,
+        on_mouse_enter: Optional[EventType[[], BASE_STATE]] = None,
+        on_mouse_leave: Optional[EventType[[], BASE_STATE]] = None,
+        on_mouse_move: Optional[EventType[[], BASE_STATE]] = None,
+        on_mouse_out: Optional[EventType[[], BASE_STATE]] = None,
+        on_mouse_over: Optional[EventType[[], BASE_STATE]] = None,
+        on_mouse_up: Optional[EventType[[], BASE_STATE]] = None,
+        on_scroll: Optional[EventType[[], BASE_STATE]] = None,
+        on_unmount: Optional[EventType[[], BASE_STATE]] = None,
+        **props,
+    ) -> "StickyLabel":
+        """Create the sticky label.
+
+        Returns:
+            The sticky label.
+        """
+        ...
+
+    def add_style(self): ...
+
+class StickyBadge(A):
+    @overload
+    @classmethod
+    def create(  # type: ignore
+        cls,
+        *children,
+        download: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        href: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        href_lang: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        media: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        ping: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        referrer_policy: Optional[
+            Union[Var[Union[bool, int, str]], bool, int, str]
+        ] = None,
+        rel: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        shape: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        target: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        access_key: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        auto_capitalize: Optional[
+            Union[Var[Union[bool, int, str]], bool, int, str]
+        ] = None,
+        content_editable: Optional[
+            Union[Var[Union[bool, int, str]], bool, int, str]
+        ] = None,
+        context_menu: Optional[
+            Union[Var[Union[bool, int, str]], bool, int, str]
+        ] = None,
+        dir: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        draggable: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        enter_key_hint: Optional[
+            Union[Var[Union[bool, int, str]], bool, int, str]
+        ] = None,
+        hidden: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        input_mode: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        item_prop: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        lang: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        role: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        slot: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        spell_check: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        tab_index: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        title: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        style: Optional[Style] = None,
+        key: Optional[Any] = None,
+        id: Optional[Any] = None,
+        class_name: Optional[Any] = None,
+        autofocus: Optional[bool] = None,
+        custom_attrs: Optional[Dict[str, Union[Var, Any]]] = None,
+        on_blur: Optional[EventType[[], BASE_STATE]] = None,
+        on_click: Optional[EventType[[], BASE_STATE]] = None,
+        on_context_menu: Optional[EventType[[], BASE_STATE]] = None,
+        on_double_click: Optional[EventType[[], BASE_STATE]] = None,
+        on_focus: Optional[EventType[[], BASE_STATE]] = None,
+        on_mount: Optional[EventType[[], BASE_STATE]] = None,
+        on_mouse_down: Optional[EventType[[], BASE_STATE]] = None,
+        on_mouse_enter: Optional[EventType[[], BASE_STATE]] = None,
+        on_mouse_leave: Optional[EventType[[], BASE_STATE]] = None,
+        on_mouse_move: Optional[EventType[[], BASE_STATE]] = None,
+        on_mouse_out: Optional[EventType[[], BASE_STATE]] = None,
+        on_mouse_over: Optional[EventType[[], BASE_STATE]] = None,
+        on_mouse_up: Optional[EventType[[], BASE_STATE]] = None,
+        on_scroll: Optional[EventType[[], BASE_STATE]] = None,
+        on_unmount: Optional[EventType[[], BASE_STATE]] = None,
+        **props,
+    ) -> "StickyBadge":
+        """Create the sticky badge.
+
+        Returns:
+            The sticky badge.
+        """
+        ...
+
+    def add_style(self): ...
+
+class StickyNamespace(ComponentNamespace):
+    label = staticmethod(StickyLabel.create)
+    logo = staticmethod(StickyLogo.create)
+
+    @staticmethod
+    def __call__(
+        *children,
+        download: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        href: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        href_lang: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        media: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        ping: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        referrer_policy: Optional[
+            Union[Var[Union[bool, int, str]], bool, int, str]
+        ] = None,
+        rel: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        shape: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        target: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        access_key: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        auto_capitalize: Optional[
+            Union[Var[Union[bool, int, str]], bool, int, str]
+        ] = None,
+        content_editable: Optional[
+            Union[Var[Union[bool, int, str]], bool, int, str]
+        ] = None,
+        context_menu: Optional[
+            Union[Var[Union[bool, int, str]], bool, int, str]
+        ] = None,
+        dir: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        draggable: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        enter_key_hint: Optional[
+            Union[Var[Union[bool, int, str]], bool, int, str]
+        ] = None,
+        hidden: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        input_mode: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        item_prop: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        lang: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        role: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        slot: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        spell_check: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        tab_index: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        title: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        style: Optional[Style] = None,
+        key: Optional[Any] = None,
+        id: Optional[Any] = None,
+        class_name: Optional[Any] = None,
+        autofocus: Optional[bool] = None,
+        custom_attrs: Optional[Dict[str, Union[Var, Any]]] = None,
+        on_blur: Optional[EventType[[], BASE_STATE]] = None,
+        on_click: Optional[EventType[[], BASE_STATE]] = None,
+        on_context_menu: Optional[EventType[[], BASE_STATE]] = None,
+        on_double_click: Optional[EventType[[], BASE_STATE]] = None,
+        on_focus: Optional[EventType[[], BASE_STATE]] = None,
+        on_mount: Optional[EventType[[], BASE_STATE]] = None,
+        on_mouse_down: Optional[EventType[[], BASE_STATE]] = None,
+        on_mouse_enter: Optional[EventType[[], BASE_STATE]] = None,
+        on_mouse_leave: Optional[EventType[[], BASE_STATE]] = None,
+        on_mouse_move: Optional[EventType[[], BASE_STATE]] = None,
+        on_mouse_out: Optional[EventType[[], BASE_STATE]] = None,
+        on_mouse_over: Optional[EventType[[], BASE_STATE]] = None,
+        on_mouse_up: Optional[EventType[[], BASE_STATE]] = None,
+        on_scroll: Optional[EventType[[], BASE_STATE]] = None,
+        on_unmount: Optional[EventType[[], BASE_STATE]] = None,
+        **props,
+    ) -> "StickyBadge":
+        """Create the sticky badge.
+
+        Returns:
+            The sticky badge.
+        """
+        ...
+
+sticky = StickyNamespace()

+ 11 - 4
reflex/components/datadisplay/logo.py

@@ -5,11 +5,15 @@ from typing import Union
 import reflex as rx
 
 
-def svg_logo(color: Union[str, rx.Var[str]] = rx.color_mode_cond("#110F1F", "white")):
+def svg_logo(
+    color: Union[str, rx.Var[str]] = rx.color_mode_cond("#110F1F", "white"),
+    **props,
+):
     """A Reflex logo SVG.
 
     Args:
         color: The color of the logo.
+        props: Extra props to pass to the svg component.
 
     Returns:
         The Reflex logo SVG.
@@ -29,11 +33,14 @@ def svg_logo(color: Union[str, rx.Var[str]] = rx.color_mode_cond("#110F1F", "whi
 
     return rx.el.svg(
         *[logo_path(d) for d in paths],
-        width="56",
-        height="12",
-        viewBox="0 0 56 12",
+        rx.el.title("Reflex"),
+        aria_label="Reflex",
+        role="img",
+        width=props.pop("width", "56"),
+        height=props.pop("height", "12"),
         fill=color,
         xmlns="http://www.w3.org/2000/svg",
+        **props,
     )
 
 

+ 3 - 0
reflex/config.py

@@ -703,6 +703,9 @@ class Config(Base):
     # Path to file containing key-values pairs to override in the environment; Dotenv format.
     env_file: Optional[str] = None
 
+    # Whether to display the sticky "Built with Reflex" badge on all pages.
+    show_built_with_reflex: bool = True
+
     # Whether the app is running in the reflex cloud environment.
     is_reflex_cloud: bool = False
 

+ 19 - 9
reflex/experimental/__init__.py

@@ -13,16 +13,25 @@ from .client_state import ClientStateVar as ClientStateVar
 from .layout import layout as layout
 from .misc import run_in_thread as run_in_thread
 
-warn(
-    "`rx._x` contains experimental features and might be removed at any time in the future .",
-)
-
-_EMITTED_PROMOTION_WARNINGS = set()
-
 
 class ExperimentalNamespace(SimpleNamespace):
     """Namespace for experimental features."""
 
+    def __getattribute__(self, item: str):
+        """Get attribute from the namespace.
+
+        Args:
+            item: attribute name.
+
+        Returns:
+            The attribute.
+        """
+        warn(
+            "`rx._x` contains experimental features and might be removed at any time in the future.",
+            dedupe=True,
+        )
+        return super().__getattribute__(item)
+
     @property
     def toast(self):
         """Temporary property returning the toast namespace.
@@ -55,9 +64,10 @@ class ExperimentalNamespace(SimpleNamespace):
         Args:
              component_name: name of the component.
         """
-        if component_name not in _EMITTED_PROMOTION_WARNINGS:
-            _EMITTED_PROMOTION_WARNINGS.add(component_name)
-            warn(f"`rx._x.{component_name}` was promoted to `rx.{component_name}`.")
+        warn(
+            f"`rx._x.{component_name}` was promoted to `rx.{component_name}`.",
+            dedupe=True,
+        )
 
 
 _x = ExperimentalNamespace(

+ 29 - 0
reflex/reflex.py

@@ -26,6 +26,8 @@ except TypeError:
     # Fallback for older typer versions.
     cli = typer.Typer(add_completion=False)
 
+SHOW_BUILT_WITH_REFLEX_INFO = "https://reflex.dev/docs/hosting/reflex-branding/"
+
 # Get the config.
 config = get_config()
 
@@ -186,6 +188,15 @@ def _run(
     prerequisites.check_latest_package_version(constants.Reflex.MODULE_NAME)
 
     if frontend:
+        if not config.show_built_with_reflex:
+            # The sticky badge may be disabled at runtime for team/enterprise tiers.
+            prerequisites.check_config_option_in_tier(
+                option_name="show_built_with_reflex",
+                allowed_tiers=["team", "enterprise"],
+                fallback_value=True,
+                help_link=SHOW_BUILT_WITH_REFLEX_INFO,
+            )
+
         # Get the app module.
         prerequisites.get_compiled_app()
 
@@ -324,6 +335,15 @@ def export(
     if prerequisites.needs_reinit(frontend=True):
         _init(name=config.app_name, loglevel=loglevel)
 
+    if frontend and not config.show_built_with_reflex:
+        # The sticky badge may be disabled on export for team/enterprise tiers.
+        prerequisites.check_config_option_in_tier(
+            option_name="show_built_with_reflex",
+            allowed_tiers=["team", "enterprise"],
+            fallback_value=False,
+            help_link=SHOW_BUILT_WITH_REFLEX_INFO,
+        )
+
     export_utils.export(
         zipping=zipping,
         frontend=frontend,
@@ -518,6 +538,15 @@ def deploy(
 
     check_version()
 
+    if not config.show_built_with_reflex:
+        # The sticky badge may be disabled on deploy for pro/team/enterprise tiers.
+        prerequisites.check_config_option_in_tier(
+            option_name="show_built_with_reflex",
+            allowed_tiers=["pro", "team", "enterprise"],
+            fallback_value=True,
+            help_link=SHOW_BUILT_WITH_REFLEX_INFO,
+        )
+
     # Set the log level.
     console.set_log_level(loglevel)
 

+ 38 - 1
reflex/utils/prerequisites.py

@@ -23,7 +23,7 @@ import zipfile
 from datetime import datetime
 from pathlib import Path
 from types import ModuleType
-from typing import Callable, List, NamedTuple, Optional
+from typing import Any, Callable, List, NamedTuple, Optional
 
 import httpx
 import typer
@@ -1978,3 +1978,40 @@ def is_generation_hash(template: str) -> bool:
         True if the template is composed of 32 or more hex characters.
     """
     return re.match(r"^[0-9a-f]{32,}$", template) is not None
+
+
+def check_config_option_in_tier(
+    option_name: str,
+    allowed_tiers: list[str],
+    fallback_value: Any,
+    help_link: str | None = None,
+):
+    """Check if a config option is allowed for the authenticated user's current tier.
+
+    Args:
+        option_name: The name of the option to check.
+        allowed_tiers: The tiers that are allowed to use the option.
+        fallback_value: The fallback value if the option is not allowed.
+        help_link: The help link to show to a user that is authenticated.
+    """
+    from reflex_cli.v2.utils import hosting
+
+    config = get_config()
+    authenticated_token = hosting.authenticated_token()
+    if not authenticated_token[0]:
+        the_remedy = (
+            "You are currently logged out. Run `reflex login` to access this option."
+        )
+        current_tier = "anonymous"
+    else:
+        current_tier = authenticated_token[1].get("tier", "").lower()
+        the_remedy = (
+            f"Your current subscription tier is `{current_tier}`. "
+            f"Please upgrade to {allowed_tiers} to access this option. "
+        )
+        if help_link:
+            the_remedy += f"See {help_link} for more information."
+    if current_tier not in allowed_tiers:
+        console.warn(f"Config option `{option_name}` is restricted. {the_remedy}")
+        setattr(config, option_name, fallback_value)
+        config._set_persistent(**{option_name: fallback_value})