diff --git a/.github/pr-assets/connection-banner-old-vs-new.png b/.github/pr-assets/connection-banner-old-vs-new.png new file mode 100644 index 00000000000..3a05c826a9a Binary files /dev/null and b/.github/pr-assets/connection-banner-old-vs-new.png differ diff --git a/packages/reflex-components-core/pyproject.toml b/packages/reflex-components-core/pyproject.toml index 349e37b1875..27bcba8e237 100644 --- a/packages/reflex-components-core/pyproject.toml +++ b/packages/reflex-components-core/pyproject.toml @@ -9,8 +9,6 @@ maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] requires-python = ">=3.10" dependencies = [ "reflex-base >= 0.9.0", - "reflex-components-lucide >= 0.9.0", - "reflex-components-sonner >= 0.9.0", "python_multipart", "starlette", "typing_extensions", @@ -31,8 +29,6 @@ targets.wheel.artifacts = ["*.pyi"] dependencies = [ "ruff", "reflex-base", - "reflex-components-lucide", - "reflex-components-sonner", "python_multipart", "starlette", "typing_extensions", diff --git a/packages/reflex-components-core/src/reflex_components_core/core/__init__.py b/packages/reflex-components-core/src/reflex_components_core/core/__init__.py index ba7d6afde67..029ce57650a 100644 --- a/packages/reflex-components-core/src/reflex_components_core/core/__init__.py +++ b/packages/reflex-components-core/src/reflex_components_core/core/__init__.py @@ -7,16 +7,6 @@ _SUBMODULES: set[str] = {"layout"} _SUBMOD_ATTRS: dict[str, list[str]] = { - "banner": [ - "ConnectionBanner", - "ConnectionModal", - "ConnectionPulser", - "ConnectionToaster", - "connection_banner", - "connection_modal", - "connection_toaster", - "connection_pulser", - ], "clipboard": ["Clipboard", "clipboard"], "colors": [ "color", diff --git a/packages/reflex-components-core/src/reflex_components_core/core/upload.py b/packages/reflex-components-core/src/reflex_components_core/core/upload.py index 9bbb28de64d..dff56bdaa04 100644 --- a/packages/reflex-components-core/src/reflex_components_core/core/upload.py +++ b/packages/reflex-components-core/src/reflex_components_core/core/upload.py @@ -32,11 +32,10 @@ from reflex_base.utils import format from reflex_base.utils.imports import ImportVar from reflex_base.vars import VarData -from reflex_base.vars.base import Var, get_unique_variable_name +from reflex_base.vars.base import LiteralVar, Var, get_unique_variable_name from reflex_base.vars.function import FunctionVar from reflex_base.vars.object import ObjectVar from reflex_base.vars.sequence import ArrayVar, LiteralStringVar -from reflex_components_sonner.toast import toast from reflex_components_core.base.fragment import Fragment from reflex_components_core.core._upload import UploadChunkIterator, UploadFile @@ -179,6 +178,10 @@ def get_upload_url(file_path: str | Var[str]) -> Var[str]: ) _on_drop_rejected_spec = passthrough_event_spec(list[dict[str, Any]]) _UPLOAD_FILES_CLIENT_HANDLER = "uploadFiles" +_toast_ref = Var( + _js_expr="refs['__toast']", + _var_data=VarData(imports={f"$/{Dirs.STATE_PATH}": [ImportVar(tag="refs")]}), +) def _default_drop_rejected(rejected_files: ArrayVar[list[dict[str, Any]]]) -> EventSpec: @@ -197,15 +200,23 @@ def _format_rejected_file_record(rf: ObjectVar[dict[str, Any]]) -> str: errors = rf["errors"].to(ArrayVar, list[dict[str, Any]]) return f"{file['path']}: {errors.foreach(lambda kv: kv['message']).join(', ')}" # noqa: FURB118 - return toast.error( - title="Files not Accepted", - description=rejected_files - .to(ArrayVar) - .foreach(_format_rejected_file_record) - .join("\n\n"), - close_button=True, - style={"white_space": "pre-line"}, + description = ( + rejected_files.to(ArrayVar).foreach(_format_rejected_file_record).join("\n\n") + ) + toast_options = LiteralVar.create({ + "title": "Files not Accepted", + "description": description, + "closeButton": True, + "style": {"whiteSpace": "pre-line"}, + }) + toast_call = Var( + _js_expr=f"{_toast_ref}?.error('', {toast_options!s})", + _var_data=VarData.merge( + _toast_ref._get_all_var_data(), + toast_options._get_all_var_data(), + ), ) + return run_script(toast_call) class UploadFilesProvider(Component): diff --git a/packages/reflex-components-sonner/pyproject.toml b/packages/reflex-components-sonner/pyproject.toml index e6db6d409c2..a283b7a970c 100644 --- a/packages/reflex-components-sonner/pyproject.toml +++ b/packages/reflex-components-sonner/pyproject.toml @@ -9,7 +9,6 @@ maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] requires-python = ">=3.10" dependencies = [ "reflex-base >= 0.9.0", - "reflex-components-lucide >= 0.9.0", ] [tool.hatch.version] @@ -24,7 +23,7 @@ targets.sdist.artifacts = ["*.pyi"] targets.wheel.artifacts = ["*.pyi"] [tool.hatch.build.hooks.reflex-pyi] -dependencies = ["ruff", "reflex-base", "reflex-components-lucide"] +dependencies = ["ruff", "reflex-base"] [build-system] requires = ["hatchling", "uv-dynamic-versioning", "hatch-reflex-pyi"] diff --git a/packages/reflex-components-sonner/src/reflex_components_sonner/toast.py b/packages/reflex-components-sonner/src/reflex_components_sonner/toast.py index f08457b526d..d8bccd986ff 100644 --- a/packages/reflex-components-sonner/src/reflex_components_sonner/toast.py +++ b/packages/reflex-components-sonner/src/reflex_components_sonner/toast.py @@ -18,7 +18,6 @@ from reflex_base.vars.function import FunctionVar from reflex_base.vars.number import ternary_operation from reflex_base.vars.object import ObjectVar -from reflex_components_lucide.icon import Icon LiteralPosition = Literal[ "top-left", @@ -213,7 +212,7 @@ class Toaster(Component): gap: Var[int] = field(doc="Gap between toasts when expanded") - loading_icon: Var[Icon] = field(doc="Changes the default loading icon") + loading_icon: Var[Component] = field(doc="Changes the default loading icon") pause_when_page_is_hidden: Var[bool] = field( doc="Pauses toast timers when the page is hidden, e.g., when the tab is backgrounded, the browser is minimized, or the OS is locked." diff --git a/pyi_hashes.json b/pyi_hashes.json index a422e6a6855..354b2c8593b 100644 --- a/pyi_hashes.json +++ b/pyi_hashes.json @@ -12,15 +12,14 @@ "packages/reflex-components-core/src/reflex_components_core/base/meta.pyi": "0cfa2d8c52321ce7440e887d03007d5b", "packages/reflex-components-core/src/reflex_components_core/base/script.pyi": "bfc7fb609b822f597d1141595f8090fe", "packages/reflex-components-core/src/reflex_components_core/base/strict_mode.pyi": "8ee129808abb4389cbd77a1736190eae", - "packages/reflex-components-core/src/reflex_components_core/core/__init__.pyi": "dd5142b3c9087bf2bf22651adf6f2724", + "packages/reflex-components-core/src/reflex_components_core/core/__init__.pyi": "c3856c8f09e2696d2c3c317726f5ad83", "packages/reflex-components-core/src/reflex_components_core/core/auto_scroll.pyi": "918dfad4d5925addd0f741e754b3b076", - "packages/reflex-components-core/src/reflex_components_core/core/banner.pyi": "6040fbada9b96c55637a9c8cc21a5e10", "packages/reflex-components-core/src/reflex_components_core/core/clipboard.pyi": "e3950e0963a6d04299ff58294687e407", "packages/reflex-components-core/src/reflex_components_core/core/debounce.pyi": "58138b5f1d5901839729d839620ea4da", "packages/reflex-components-core/src/reflex_components_core/core/helmet.pyi": "7fd81a99bde5b0ff94bb52523597fd5c", "packages/reflex-components-core/src/reflex_components_core/core/html.pyi": "753d6ae315369530dad450ed643f5be6", "packages/reflex-components-core/src/reflex_components_core/core/sticky.pyi": "ba60a7d9cba75b27a1133bd63a9fbd59", - "packages/reflex-components-core/src/reflex_components_core/core/upload.pyi": "2dd6ba6e3a4d61fc1d79eb582a7cc548", + "packages/reflex-components-core/src/reflex_components_core/core/upload.pyi": "5a4d5533a01f7a56a802e00615247873", "packages/reflex-components-core/src/reflex_components_core/core/window_events.pyi": "5e1dcb1130bc8af282783fae329ae6a6", "packages/reflex-components-core/src/reflex_components_core/datadisplay/__init__.pyi": "c96fed4da42a13576d64f84e3c7cb25c", "packages/reflex-components-core/src/reflex_components_core/el/__init__.pyi": "f09129ddefb57ab4c7769c86dc9a3153", @@ -117,8 +116,9 @@ "packages/reflex-components-recharts/src/reflex_components_recharts/general.pyi": "5a1a479924ad6184abafe4d796cb04c5", "packages/reflex-components-recharts/src/reflex_components_recharts/polar.pyi": "1979bb6c22bb7a0d3342b2d63fb19d74", "packages/reflex-components-recharts/src/reflex_components_recharts/recharts.pyi": "c5288f311fe37b23539518ba2a3d4482", - "packages/reflex-components-sonner/src/reflex_components_sonner/toast.pyi": "2c5fadcc014056f041cd4d916137d9e7", - "reflex/__init__.pyi": "3a9bb8544cbc338ffaf0a5927d9156df", + "packages/reflex-components-sonner/src/reflex_components_sonner/toast.pyi": "e623d84fde45af974de486dd147eccdf", + "reflex/__init__.pyi": "314c91dbc2d7d60bb2d754ac5a22b310", "reflex/components/__init__.pyi": "f39a2af77f438fa243c58c965f19d42e", + "reflex/components/banner.pyi": "48126bffb8055e79c2f03272ed15eedd", "reflex/experimental/memo.pyi": "d09629b81bf0df6153b131ac0ee10bd7" } diff --git a/reflex/__init__.py b/reflex/__init__.py index 3a81b098db6..5608900b577 100644 --- a/reflex/__init__.py +++ b/reflex/__init__.py @@ -100,7 +100,7 @@ from reflex_components_radix.mappings import RADIX_MAPPING # noqa: E402 _COMPONENTS_CORE_MAPPING: lazy_loader.SubmodAttrsType = { - "reflex_components_core.core.banner": [ + "reflex.components.banner": [ "connection_banner", "connection_modal", ], diff --git a/reflex/app.py b/reflex/app.py index 847f8f24ecf..0596267d97e 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -47,11 +47,6 @@ from reflex_base.utils.types import ASGIApp, Message, Receive, Scope, Send from reflex_components_core.base.error_boundary import ErrorBoundary from reflex_components_core.base.fragment import Fragment -from reflex_components_core.core.banner import ( - backend_disabled, - connection_pulser, - connection_toaster, -) from reflex_components_core.core.breakpoints import set_breakpoints from reflex_components_core.core.sticky import sticky from reflex_components_sonner.toast import toast @@ -70,6 +65,11 @@ from reflex.app_mixins import AppMixin, LifespanMixin, MiddlewareMixin from reflex.compiler import compiler from reflex.compiler.compiler import readable_name_from_component +from reflex.components.banner import ( + backend_disabled, + connection_pulser, + connection_toaster, +) from reflex.istate.data import RouterData from reflex.istate.manager import StateManager, StateModificationContext from reflex.istate.manager.token import BaseStateToken diff --git a/packages/reflex-components-core/src/reflex_components_core/core/banner.py b/reflex/components/banner.py similarity index 90% rename from packages/reflex-components-core/src/reflex_components_core/core/banner.py rename to reflex/components/banner.py index 8d9af0bef76..3e3738d3b24 100644 --- a/packages/reflex-components-core/src/reflex_components_core/core/banner.py +++ b/reflex/components/banner.py @@ -2,6 +2,8 @@ from __future__ import annotations +from typing import cast + from reflex_base import constants from reflex_base.components.component import Component from reflex_base.constants import Dirs, Hooks, Imports @@ -13,13 +15,18 @@ from reflex_base.vars.function import FunctionStringVar from reflex_base.vars.number import BooleanVar from reflex_base.vars.sequence import LiteralArrayVar -from reflex_components_lucide.icon import Icon -from reflex_components_sonner.toast import ToastProps, toast_ref - -from reflex_components_core import el from reflex_components_core.base.fragment import Fragment from reflex_components_core.core.cond import cond +from reflex_components_core.el.elements.forms import Button +from reflex_components_core.el.elements.inline import Span +from reflex_components_core.el.elements.media import Path, Svg +from reflex_components_core.el.elements.metadata import Link +from reflex_components_core.el.elements.other import Dialog +from reflex_components_core.el.elements.sectioning import H2 from reflex_components_core.el.elements.typography import Div +from reflex_components_core.react_router.dom import ReactRouterLink +from reflex_components_lucide.icon import Icon +from reflex_components_sonner.toast import ToastProps, toast_ref connect_error_var_data: VarData = VarData( imports=Imports.EVENTS, @@ -204,9 +211,9 @@ def create(cls, comp: Component | None = None) -> Component: Returns: The connection banner component. """ - if not comp: - comp = el.div( - el.span( + if comp is None: + comp = Div.create( + Span.create( *default_connection_error(), color="black", font_size="1.125rem", @@ -235,12 +242,12 @@ def create(cls, comp: Component | None = None) -> Component: Returns: The connection banner component. """ - if not comp: - comp = el.span(*default_connection_error()) + if comp is None: + comp = Span.create(*default_connection_error()) return cond( has_too_many_connection_errors, - el.dialog( - el.h2("Connection Error"), + Dialog.create( + H2.create("Connection Error"), comp, open=has_too_many_connection_errors, z_index=9999, @@ -265,16 +272,19 @@ def create(cls, *children, **props) -> Icon: pulse_var = Var(r"keyframes({ from: { opacity: 0 }, to: { opacity: 1 } })").to( str ) - return super().create( - "wifi_off", - color=props.pop("color", "crimson"), - size=props.pop("size", 32), - z_index=props.pop("z_index", 9999), - position=props.pop("position", "fixed"), - bottom=props.pop("bottom", "33px"), - right=props.pop("right", "33px"), - animation=LiteralVar.create(f"{pulse_var} 1s infinite"), - **props, + return cast( + Icon, + super().create( + "wifi_off", + color=props.pop("color", "crimson"), + size=props.pop("size", 32), + z_index=props.pop("z_index", 9999), + position=props.pop("position", "fixed"), + bottom=props.pop("bottom", "33px"), + right=props.pop("right", "33px"), + animation=LiteralVar.create(f"{pulse_var} 1s infinite"), + **props, + ), ) def add_imports(self) -> dict[str, str | ImportVar | list[str | ImportVar]]: @@ -342,8 +352,8 @@ def create(cls, **props) -> Component: ), ) - warning_icon = el.svg( - el.path( + warning_icon = Svg.create( + Path.create( d="M6.90816 1.34341C7.61776 1.10786 8.38256 1.10786 9.09216 1.34341C9.7989 1.57799 10.3538 2.13435 10.9112 2.91605C11.4668 3.69515 12.0807 4.78145 12.872 6.18175L12.9031 6.23672C13.6946 7.63721 14.3085 8.72348 14.6911 9.60441C15.0755 10.4896 15.267 11.2539 15.1142 11.9881C14.9604 12.7275 14.5811 13.3997 14.0287 13.9079C13.4776 14.4147 12.7273 14.6286 11.7826 14.7313C10.8432 14.8334 9.6143 14.8334 8.0327 14.8334H7.9677C6.38604 14.8334 5.15719 14.8334 4.21778 14.7313C3.27301 14.6286 2.52269 14.4147 1.97164 13.9079C1.41924 13.3997 1.03995 12.7275 0.88613 11.9881C0.733363 11.2539 0.92483 10.4896 1.30926 9.60441C1.69184 8.72348 2.30573 7.63721 3.09722 6.23671L3.12828 6.18175C3.91964 4.78146 4.53355 3.69515 5.08914 2.91605C5.64658 2.13435 6.20146 1.57799 6.90816 1.34341ZM7.3335 11.3334C7.3335 10.9652 7.63063 10.6667 7.99716 10.6667H8.00316C8.3697 10.6667 8.66683 10.9652 8.66683 11.3334C8.66683 11.7016 8.3697 12.0001 8.00316 12.0001H7.99716C7.63063 12.0001 7.3335 11.7016 7.3335 11.3334ZM7.3335 8.66675C7.3335 9.03495 7.63196 9.33341 8.00016 9.33341C8.36836 9.33341 8.66683 9.03495 8.66683 8.66675V6.00008C8.66683 5.63189 8.36836 5.33341 8.00016 5.33341C7.63196 5.33341 7.3335 5.63189 7.3335 6.00008V8.66675Z", fill_rule="evenodd", clip_rule="evenodd", @@ -358,10 +368,10 @@ def create(cls, **props) -> Component: flex_shrink="0", ) - info_message = el.div( - el.span( + info_message = Div.create( + Span.create( "If you are the owner of this app, visit ", - el.a( + ReactRouterLink.create( "Reflex Cloud", color=color("amber", 11), text_decoration="underline", @@ -393,8 +403,8 @@ def create(cls, **props) -> Component: # Prepend warning icon into info_message children info_message.children.insert(0, warning_icon) - resume_button = el.a( - el.button( + resume_button = ReactRouterLink.create( + Button.create( "Resume app", color="rgba(252, 252, 253, 1)", font_size="0.875rem", @@ -416,9 +426,9 @@ def create(cls, **props) -> Component: target="_blank", ) - card = el.div( - el.div( - el.div( + card = Div.create( + Div.create( + Div.create( "This app is paused", font_size="1.5rem", font_weight="600", @@ -449,17 +459,17 @@ def create(cls, **props) -> Component: return super().create( cond( is_backend_disabled, - el.div( - el.link( + Div.create( + Link.create( rel="preconnect", href="https://fonts.googleapis.com", ), - el.link( + Link.create( rel="preconnect", href="https://fonts.gstatic.com", crossorigin="", ), - el.link( + Link.create( href="https://fonts.googleapis.com/css2?family=Instrument+Sans:ital,wght@0,500;0,600&display=swap", rel="stylesheet", ), diff --git a/reflex/utils/codespaces.py b/reflex/utils/codespaces.py index 00e9ab7ba75..6fdbdd01160 100644 --- a/reflex/utils/codespaces.py +++ b/reflex/utils/codespaces.py @@ -8,11 +8,12 @@ from reflex_base.constants import Endpoint from reflex_base.utils.decorator import once from reflex_components_core.base.script import Script -from reflex_components_core.core.banner import has_connection_errors from reflex_components_core.core.cond import cond from starlette.requests import Request from starlette.responses import HTMLResponse +from reflex.components.banner import has_connection_errors + @once def redirect_script() -> str: diff --git a/tests/units/components/core/test_banner.py b/tests/units/components/core/test_banner.py index 1df213f5936..112750c7fee 100644 --- a/tests/units/components/core/test_banner.py +++ b/tests/units/components/core/test_banner.py @@ -1,10 +1,11 @@ -from reflex_components_core.core.banner import ( +from reflex_components_radix.themes.typography.text import Text + +from reflex.components.banner import ( ConnectionBanner, ConnectionModal, ConnectionPulser, WebsocketTargetURL, ) -from reflex_components_radix.themes.typography.text import Text def test_websocket_target_url(): diff --git a/tests/units/components/test_package_dependencies.py b/tests/units/components/test_package_dependencies.py new file mode 100644 index 00000000000..198608d7fc1 --- /dev/null +++ b/tests/units/components/test_package_dependencies.py @@ -0,0 +1,56 @@ +from __future__ import annotations + +import sys +from pathlib import Path + +if sys.version_info >= (3, 11): + import tomllib +else: + import tomli as tomllib + + +REPO_ROOT = Path(__file__).resolve().parents[3] + + +def _package_dependencies(package: str) -> list[str]: + pyproject = REPO_ROOT / "packages" / package / "pyproject.toml" + data = tomllib.loads(pyproject.read_text()) + + dependencies = list(data["project"].get("dependencies", [])) + dependencies.extend( + data + .get("tool", {}) + .get("hatch", {}) + .get("build", {}) + .get("hooks", {}) + .get("reflex-pyi", {}) + .get("dependencies", []) + ) + return dependencies + + +def _dependency_names(package: str) -> set[str]: + return {dependency.split()[0] for dependency in _package_dependencies(package)} + + +def _source_imports(package: str, import_name: str) -> bool: + source_root = REPO_ROOT / "packages" / package / "src" + return any( + import_name in source.read_text() for source in source_root.rglob("*.py") + ) + + +def test_core_does_not_depend_on_lucide_or_sonner(): + dependency_names = _dependency_names("reflex-components-core") + + assert "reflex-components-lucide" not in dependency_names + assert "reflex-components-sonner" not in dependency_names + assert not _source_imports("reflex-components-core", "reflex_components_lucide") + assert not _source_imports("reflex-components-core", "reflex_components_sonner") + + +def test_sonner_does_not_depend_on_lucide(): + dependency_names = _dependency_names("reflex-components-sonner") + + assert "reflex-components-lucide" not in dependency_names + assert not _source_imports("reflex-components-sonner", "reflex_components_lucide") diff --git a/uv.lock b/uv.lock index f3122ca7c5b..c3c0befd6c4 100644 --- a/uv.lock +++ b/uv.lock @@ -3588,8 +3588,6 @@ source = { editable = "packages/reflex-components-core" } dependencies = [ { name = "python-multipart" }, { name = "reflex-base" }, - { name = "reflex-components-lucide" }, - { name = "reflex-components-sonner" }, { name = "starlette" }, { name = "typing-extensions" }, ] @@ -3598,8 +3596,6 @@ dependencies = [ requires-dist = [ { name = "python-multipart" }, { name = "reflex-base", editable = "packages/reflex-base" }, - { name = "reflex-components-lucide", editable = "packages/reflex-components-lucide" }, - { name = "reflex-components-sonner", editable = "packages/reflex-components-sonner" }, { name = "starlette" }, { name = "typing-extensions" }, ] @@ -3735,14 +3731,10 @@ name = "reflex-components-sonner" source = { editable = "packages/reflex-components-sonner" } dependencies = [ { name = "reflex-base" }, - { name = "reflex-components-lucide" }, ] [package.metadata] -requires-dist = [ - { name = "reflex-base", editable = "packages/reflex-base" }, - { name = "reflex-components-lucide", editable = "packages/reflex-components-lucide" }, -] +requires-dist = [{ name = "reflex-base", editable = "packages/reflex-base" }] [[package]] name = "reflex-docgen"