diff --git a/docs/ai_builder/features/design_systems.md b/docs/ai_builder/features/design_systems.md new file mode 100644 index 00000000000..3110d7ec67b --- /dev/null +++ b/docs/ai_builder/features/design_systems.md @@ -0,0 +1,85 @@ +# Design Systems + +Design Systems give Reflex Build reusable visual guidelines for every app in a project. Define colors, typography, spacing, component styles, and other brand rules once, then let the AI Builder apply them as it creates and updates your apps. + +## Open Design Systems + +You can access Design Systems in two places: + +- In your project, open **Project Settings** and select **Design Systems** to create and manage the project's design systems. +- In the new-app prompt box, open the **Design System** selector to choose an existing design system or create one without leaving the builder. + +Design systems belong to the project, so every app in that project can reuse them. + +## Create a Design System + +From **Project Settings > Design Systems**, click **Create new**. Enter a name and provide at least one source for the design: + +- **Guidelines**: Describe the visual direction in plain language. +- **Reference website**: Enter a public website URL. Reflex captures a screenshot of the page and uses its visual language as a reference. +- **Reference file**: Upload a PDF or image containing a brand guide, style guide, screenshot, or mockup. + +You can combine written guidelines with a website or uploaded file. Click **Generate** to create and save the design system. The new design system becomes active automatically. + +For example: + +```text +Use a warm, editorial style with cream backgrounds, dark green text, +serif headings, compact navigation, and orange accents for primary actions. +Keep cards flat with thin borders and use generous spacing between sections. +``` + +### Supported Reference Files + +Reflex accepts one reference file up to 20 MB in any of these formats: + +- PDF +- PNG +- JPG or JPEG +- WebP + +## Start From an Example + +The **Examples** section includes ready-made design systems such as Minimal, Modern, Material, Carbon, Neobrutalism, and Glassmorphism. Select **Add Style** on an example to add it to the project and make it active. + +Examples are useful as a starting point. After adding one, edit its saved Markdown instructions to match your brand more closely. + +## Select the Active Design System + +Enable **Auto-enable** next to a saved design system to make it active for subsequent AI Builder requests in the project. You can also select or create a design system from the new-app prompt box before generating an app. + +Only one design system can be active at a time. Activating, creating, or adding another design system automatically deactivates the current one. Turn off the active design system if you want to build without design-system guidance. + +The active design system also guides later updates to the project's apps. You can still provide page-specific requirements in your prompt: + +```text +Use the active design system, but make this checkout page more compact +and reserve the accent color for the final purchase action. +``` + +## Edit or Delete a Design System + +In **Project Settings > Design Systems**, open the menu next to a saved design system. + +- Select **Edit** to change its name or update the Markdown instructions that the AI Builder follows. +- Select **Delete** to remove it from the project. + +Editing the Markdown is useful when you need exact tokens or component rules: + +```markdown +## Buttons + +- Primary buttons use `#0F62FE` with white text. +- Use a 4 px corner radius. +- Button labels use 14 px semibold text. +- Destructive actions use `#DA1E28`. +``` + +## Best Practices + +- Give each design system a descriptive name, such as `Marketing Site` or `Internal Admin Tools`. +- Describe concrete choices such as colors, typefaces, spacing, borders, and component behavior instead of asking for a design that is only "modern" or "clean." +- Use a public, visually representative page when supplying a reference website. +- Upload a focused brand guide or screenshot that clearly shows the styles you want the AI Builder to reproduce. +- Review the saved Markdown and add any non-negotiable accessibility or brand requirements. +- Keep one design system active while building a consistent group of apps, and switch it when the project needs a different visual direction. diff --git a/docs/app/reflex_docs/templates/docpage/sidebar/sidebar_items/ai.py b/docs/app/reflex_docs/templates/docpage/sidebar/sidebar_items/ai.py index 883523f49e6..c4b72e36678 100644 --- a/docs/app/reflex_docs/templates/docpage/sidebar/sidebar_items/ai.py +++ b/docs/app/reflex_docs/templates/docpage/sidebar/sidebar_items/ai.py @@ -28,6 +28,7 @@ def get_sidebar_items_ai_builder_overview(): ai_builder.features.connect_to_github, ai_builder.features.connect_to_git_providers, ai_builder.features.knowledge, + ai_builder.features.design_systems, ai_builder.features.image_as_prompt, # ai_builder.features.automated_testing, ai_builder.features.customization, diff --git a/docs/app/reflex_docs/views/docs_navbar.py b/docs/app/reflex_docs/views/docs_navbar.py index 91a9bef139a..aea1fe95652 100644 --- a/docs/app/reflex_docs/views/docs_navbar.py +++ b/docs/app/reflex_docs/views/docs_navbar.py @@ -18,7 +18,7 @@ def github_button() -> rx.Component: label = f"View Reflex on GitHub - {GITHUB_STARS // 1000}K stars" - return rx.el.a( + return rx.el.elements.a( button( get_icon(icon="github_navbar", class_name="shrink-0"), f"{GITHUB_STARS // 1000}K", @@ -65,7 +65,9 @@ def logo() -> rx.Component: ) -def menu_item(text: str, href: str, active_str: str = "") -> rx.Component: +def menu_item( + text: str, href: str, active_str: str = "", external: bool = False +) -> rx.Component: router_path = rx.State.router.page.path active_cn = "shadow-[inset_0_-1px_0_0_var(--primary-10)] [&_button]:text-primary-10 [&_div]:text-primary-10" @@ -92,8 +94,10 @@ def menu_item(text: str, href: str, active_str: str = "") -> rx.Component: else: active = router_path.contains(active_str) + anchor = rx.el.elements.a if external else rx.el.a + return ui.navigation_menu.item( - rx.el.elements.a( + anchor( button( text, size="sm", @@ -117,7 +121,7 @@ def navigation_menu() -> rx.Component: menu_item("Build with AI", ai_builder.overview.best_practices.path, "ai"), menu_item("Framework", getting_started.introduction.path, "framework"), menu_item("Cloud", hosting.deploy_quick_start.path, "hosting"), - menu_item("XY", "/docs/xy/", "xy"), + menu_item("XY", "/docs/xy/", "xy", external=True), class_name="flex flex-row items-center gap-2 m-0 h-full list-none", custom_attrs={"role": "menubar"}, ), diff --git a/docs/app/tests/test_docs_navbar.py b/docs/app/tests/test_docs_navbar.py new file mode 100644 index 00000000000..b33630de7b2 --- /dev/null +++ b/docs/app/tests/test_docs_navbar.py @@ -0,0 +1,126 @@ +"""Tests for the docs navbar links. + +The docs app is served under ``frontend_path="/docs"``. ``rx.el.a`` compiles to +React Router's ``Link``, which resolves its destination against that basename; +``rx.el.elements.a`` stays a raw HTML anchor, which does not. In-app links must +therefore use the former, and links that already carry a full path (other +deployments, the marketing site) must use the latter. +""" + +import pytest +import reflex as rx + + +@pytest.fixture +def navbar(): + """Import the navbar module through the pages package. + + Importing ``reflex_docs.views.docs_navbar`` first in a fresh interpreter + trips a pre-existing circular import via ``reflex_docs.pages``. + + Yields: + The ``reflex_docs.views.docs_navbar`` module. + """ + import reflex_docs.pages # noqa: F401 + from reflex_docs.views import docs_navbar + + yield docs_navbar + + +def _collect_links(component) -> list[tuple[str, str]]: + """Walk a component tree and collect every anchor destination. + + Walks the tree rather than matching against ``str(component)``, whose repr + is truncated for a tree this size. + + Args: + component: The component to walk. + + Returns: + A list of ``(kind, destination)`` pairs, where kind is ``"router"`` for + a React Router link and ``"anchor"`` for a raw HTML anchor. + """ + links = [] + name = type(component).__name__ + if name == "ReactRouterLink": + links.append(("router", str(component.to).strip('"'))) + elif name == "A": + links.append(("anchor", str(component.href).strip('"'))) + for child in getattr(component, "children", ()): + links.extend(_collect_links(child)) + return links + + +def test_internal_menu_items_use_router_links(navbar): + """In-app navbar links must compile to React Router links. + + Regression test: as a raw anchor, "Build with AI" sent users to + ``reflex.dev/ai/overview/best-practices/`` instead of + ``reflex.dev/docs/ai/overview/best-practices/``. + """ + links = _collect_links( + navbar.menu_item("Build with AI", "/ai/overview/best-practices/", "ai") + ) + + assert links == [("router", "/ai/overview/best-practices/")] + + +def test_external_menu_items_use_plain_anchors(navbar): + """Cross-app navbar links must stay raw anchors that own their full path.""" + links = _collect_links(navbar.menu_item("XY", "/docs/xy/", "xy", external=True)) + + assert links == [("anchor", "/docs/xy/")] + + +def test_navigation_menu_routes_in_app_destinations(navbar): + """Every in-app navbar destination compiles to a router link.""" + from reflex_docs.pages.docs import ai_builder, getting_started, hosting + + router_targets = { + dest + for kind, dest in _collect_links(navbar.navigation_menu()) + if kind == "router" + } + + for path in ( + "/", + ai_builder.overview.best_practices.path, + getting_started.introduction.path, + hosting.deploy_quick_start.path, + ): + assert path in router_targets, f"{path} is not a router link" + + # Router links resolve against frontend_path, so a literal /docs prefix + # here would compile to /docs/docs/... + double_prefixed = [dest for dest in router_targets if dest.startswith("/docs")] + assert not double_prefixed, f"double-prefixed router links: {double_prefixed}" + + +def test_navigation_menu_keeps_cross_app_destinations_raw(navbar): + """Destinations outside this app render as raw anchors.""" + anchor_targets = { + dest + for kind, dest in _collect_links(navbar.navigation_menu()) + if kind == "anchor" + } + + assert "/docs/xy/" in anchor_targets + + +def test_external_links_bypass_the_router(navbar): + """Absolute off-site URLs render as raw anchors, not router links.""" + from reflex_site_shared.constants import GITHUB_URL, REFLEX_URL + + assert _collect_links(navbar.github_button()) == [("anchor", GITHUB_URL)] + assert _collect_links(navbar.logo()) == [("anchor", REFLEX_URL)] + + +def test_reflex_el_a_and_elements_a_are_not_interchangeable(): + """Guard the distinction the navbar relies on. + + ``rx.el.a`` is aliased to React Router's ``Link``; ``rx.el.elements.a`` is + the raw HTML anchor. If those ever converge, the navbar's internal vs. + external split becomes meaningless and this test should be revisited. + """ + assert _collect_links(rx.el.a(href="/x/")) == [("router", "/x/")] + assert _collect_links(rx.el.elements.a(href="/x/")) == [("anchor", "/x/")]