diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index bdf884a..31c6c2e 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -5,16 +5,16 @@ repos:
rev: v0.15.5
hooks:
- id: ruff-check
- files: ^reflex_ui/
+ files: ^(reflex_ui|shared)/
args: ["--fix", "--exit-non-zero-on-fix", "--no-unsafe-fixes"]
- id: ruff-format
- files: ^reflex_ui/
+ files: ^(reflex_ui|shared)/
- repo: https://github.com/codespell-project/codespell
rev: v2.4.2
hooks:
- id: codespell
- files: ^reflex_ui/
+ files: ^(reflex_ui|shared)/
# Run pyi check before pyright because pyright can fail if pyi files are wrong.
# - repo: local
@@ -31,5 +31,5 @@ repos:
rev: v1.1.408
hooks:
- id: pyright
- files: ^reflex_ui/
+ files: ^(reflex_ui|shared)/
language: system
diff --git a/pyproject.toml b/pyproject.toml
index bdbab45..b9fbf04 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -69,6 +69,7 @@ lint.pydocstyle.convention = "google"
"*.pyi" = ["D301", "D415", "D417", "D418", "E742", "N", "PGH"]
"**/alembic/*.py" = ["D", "ERA"]
"__init__.py" = ["ERA"]
+"shared/**" = ["D100", "D101", "D102", "D103", "D104", "T201"]
[tool.pyright]
reportIncompatibleMethodOverride = false
diff --git a/shared/__init__.py b/shared/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/shared/backend/__init__.py b/shared/backend/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/shared/backend/get_blogs.py b/shared/backend/get_blogs.py
new file mode 100644
index 0000000..48970c3
--- /dev/null
+++ b/shared/backend/get_blogs.py
@@ -0,0 +1,37 @@
+from typing import TypedDict
+
+import httpx
+import reflex as rx
+
+from shared.constants import RECENT_BLOGS_API_URL
+
+
+class BlogPostDict(TypedDict):
+ title: str
+ description: str
+ author: str
+ date: str
+ image: str
+ tag: str
+ url: str
+
+
+class RecentBlogsState(rx.State):
+ posts: rx.Field[list[BlogPostDict]] = rx.field(default_factory=list)
+ _fetched: bool = False
+
+ @rx.event(background=True, temporal=True)
+ async def fetch_recent_blogs(self):
+ if self._fetched:
+ return
+ try:
+ async with httpx.AsyncClient() as client:
+ resp = await client.get(RECENT_BLOGS_API_URL, timeout=10)
+ resp.raise_for_status()
+ data = resp.json()
+ async with self:
+ self.posts = data.get("posts", [])
+ self._fetched = True
+ except Exception:
+ async with self:
+ self.posts = []
diff --git a/shared/backend/signup.py b/shared/backend/signup.py
new file mode 100644
index 0000000..dd2c1ff
--- /dev/null
+++ b/shared/backend/signup.py
@@ -0,0 +1,111 @@
+import contextlib
+import os
+from datetime import datetime
+from typing import Any
+
+import httpx
+import reflex as rx
+from email_validator import EmailNotValidError, ValidatedEmail, validate_email
+from sqlmodel import Field
+
+from shared.constants import (
+ API_BASE_URL_LOOPS,
+ REFLEX_DEV_WEB_NEWSLETTER_FORM_WEBHOOK_URL,
+)
+
+
+class Waitlist(rx.Model, table=True):
+ email: str
+ date_created: datetime = Field(default_factory=datetime.utcnow, nullable=False)
+
+
+class IndexState(rx.State):
+ """Hold the state for the home page."""
+
+ # Whether the user signed up for the newsletter.
+ signed_up: bool = False
+
+ # Whether to show the confetti.
+ show_confetti: bool = False
+
+ @rx.event(background=True)
+ async def send_contact_to_webhook(
+ self,
+ email: str | None,
+ ) -> None:
+ with contextlib.suppress(httpx.HTTPError):
+ async with httpx.AsyncClient() as client:
+ response = await client.post(
+ REFLEX_DEV_WEB_NEWSLETTER_FORM_WEBHOOK_URL,
+ json={
+ "email": email,
+ },
+ )
+ response.raise_for_status()
+
+ @rx.event(background=True)
+ async def add_contact_to_loops(
+ self,
+ email: str | None,
+ ):
+ url: str = f"{API_BASE_URL_LOOPS}/contacts/create"
+ loops_api_key: str | None = os.getenv("LOOPS_API_KEY")
+ if loops_api_key is None:
+ print("Loops API key does not exist")
+ return
+
+ headers = {
+ "Accept": "application/json",
+ "Authorization": f"Bearer {loops_api_key}",
+ }
+ try:
+ async with httpx.AsyncClient() as client:
+ response = await client.post(
+ url,
+ headers=headers,
+ json={
+ "email": email,
+ },
+ )
+ response.raise_for_status() # Raise an exception for HTTP errors (4xx and 5xx)
+
+ except httpx.HTTPError as e:
+ print(f"An error occurred: {e}")
+
+ @rx.event
+ def signup_for_another_user(self):
+ self.signed_up = False
+
+ @rx.event(background=True)
+ async def signup(
+ self,
+ form_data: dict[str, Any],
+ ):
+ """Sign the user up for the newsletter."""
+ email: str | None = None
+ if email_to_validate := form_data.get("input_email"):
+ try:
+ validated_email: ValidatedEmail = validate_email(
+ email_to_validate,
+ check_deliverability=True,
+ )
+ email = validated_email.normalized
+
+ except EmailNotValidError as e:
+ # Alert the error message.
+ yield rx.toast.warning(
+ str(e),
+ style={
+ "border": "1px solid #3C3646",
+ "background": "linear-gradient(218deg, #1D1B23 -35.66%, #131217 100.84%)",
+ },
+ )
+ return
+ yield IndexState.send_contact_to_webhook(email)
+ yield IndexState.add_contact_to_loops(email)
+ async with self:
+ self.signed_up = True
+ yield
+ yield [
+ rx.toast.success("Thanks for signing up to the Newsletter!"),
+ ]
diff --git a/shared/components/__init__.py b/shared/components/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/shared/components/blocks/__init__.py b/shared/components/blocks/__init__.py
new file mode 100644
index 0000000..9e8ba3c
--- /dev/null
+++ b/shared/components/blocks/__init__.py
@@ -0,0 +1,4 @@
+from .code import *
+from .demo import *
+from .headings import *
+from .typography import *
diff --git a/shared/components/blocks/code.py b/shared/components/blocks/code.py
new file mode 100644
index 0000000..9211332
--- /dev/null
+++ b/shared/components/blocks/code.py
@@ -0,0 +1,93 @@
+"""Code block components for documentation pages."""
+
+import reflex as rx
+
+import shared.styles.fonts as fonts
+from shared import styles
+
+
+@rx.memo
+def code_block(code: str, language: str):
+ return rx.box(
+ rx._x.code_block(
+ code,
+ language=language,
+ class_name="code-block",
+ can_copy=True,
+ ),
+ class_name="relative mb-4",
+ )
+
+
+@rx.memo
+def code_block_dark(code: str, language: str):
+ return rx.box(
+ rx._x.code_block(
+ code,
+ language=language,
+ class_name="code-block",
+ can_copy=True,
+ ),
+ class_name="relative mb-4",
+ )
+
+
+def code_block_markdown(*children, **props):
+ language = props.get("language", "plain")
+ return code_block(code=children[0], language=language)
+
+
+def code_block_markdown_dark(*children, **props):
+ language = props.get("language", "plain")
+ return code_block_dark(code=children[0], language=language)
+
+
+def doccmdoutput(
+ command: str,
+ output: str,
+) -> rx.Component:
+ """Create a documentation code snippet.
+
+ Args:
+ command: The command to display.
+ output: The output of the command.
+ theme: The theme of the component.
+
+ Returns:
+ The styled command and its example output.
+ """
+ return rx.vstack(
+ rx._x.code_block(
+ command,
+ can_copy=True,
+ border_radius=styles.DOC_BORDER_RADIUS,
+ background="transparent",
+ theme="ayu-dark",
+ language="bash",
+ code_tag_props={
+ "style": {
+ "fontFamily": "inherit",
+ }
+ },
+ style=fonts.code,
+ font_family="JetBrains Mono",
+ width="100%",
+ ),
+ rx._x.code_block(
+ output,
+ can_copy=False,
+ border_radius="12px",
+ background="transparent",
+ theme="ayu-dark",
+ language="log",
+ code_tag_props={
+ "style": {
+ "fontFamily": "inherit",
+ }
+ },
+ style=fonts.code,
+ font_family="JetBrains Mono",
+ width="100%",
+ ),
+ padding_y="1em",
+ )
diff --git a/shared/components/blocks/collapsible.py b/shared/components/blocks/collapsible.py
new file mode 100644
index 0000000..18f0840
--- /dev/null
+++ b/shared/components/blocks/collapsible.py
@@ -0,0 +1,58 @@
+"""Collapsible accordion box used by alert and video blocks."""
+
+from collections.abc import Sequence
+
+import reflex as rx
+from reflex_core.constants.colors import ColorType
+
+
+def collapsible_box(
+ trigger_children: Sequence[rx.Component],
+ body: rx.Component,
+ color: ColorType,
+ *,
+ item_border_radius: str = "12px",
+) -> rx.Component:
+ """Collapsible accordion wrapper shared by alert and video directives."""
+ return rx.box(
+ rx.accordion.root(
+ rx.accordion.item(
+ rx.accordion.header(
+ rx.accordion.trigger(
+ rx.hstack(
+ *trigger_children,
+ rx.spacer(),
+ rx.accordion.icon(color=f"{rx.color(color, 11)}"),
+ align_items="center",
+ justify_content="left",
+ text_align="left",
+ spacing="2",
+ width="100%",
+ ),
+ padding="0px",
+ color=f"{rx.color(color, 11)} !important",
+ background_color="transparent !important",
+ border_radius="12px",
+ _hover={},
+ ),
+ ),
+ body,
+ border_radius=item_border_radius,
+ padding=["16px", "24px"],
+ background_color=f"{rx.color(color, 3)}",
+ border="none",
+ ),
+ background="transparent !important",
+ border_radius="12px",
+ box_shadow="none !important",
+ collapsible=True,
+ width="100%",
+ ),
+ border=f"1px solid {rx.color(color, 4)}",
+ border_radius="12px",
+ background_color=f"{rx.color(color, 3)} !important",
+ width="100%",
+ margin_bottom="16px",
+ margin_top="16px",
+ overflow="hidden",
+ )
diff --git a/shared/components/blocks/demo.py b/shared/components/blocks/demo.py
new file mode 100644
index 0000000..30fa7e2
--- /dev/null
+++ b/shared/components/blocks/demo.py
@@ -0,0 +1,167 @@
+"""Components for rendering code demos in the documentation."""
+
+import textwrap
+from typing import Any
+
+import reflex as rx
+import ruff_format
+
+from .code import code_block, code_block_dark
+
+
+def docdemobox(*children, **props) -> rx.Component:
+ """Create a documentation demo box with the output of the code.
+
+ Args:
+ children: The children to display.
+ props: Additional props to apply to the box.
+
+ Returns:
+ The styled demo box.
+ """
+ return rx.box(
+ *children,
+ **props,
+ class_name="flex flex-col p-6 rounded-xl overflow-x-auto border border-slate-4 bg-slate-2 items-center justify-center w-full",
+ )
+
+
+def doccode(
+ code: str,
+ language: str = "python",
+ lines: tuple[int, int] | None = None,
+ theme: str = "light",
+) -> rx.Component:
+ """Create a documentation code snippet.
+
+ Args:
+ code: The code to display.
+ language: The language of the code.
+ lines: The start/end lines to display.
+ theme: The theme for the code snippet.
+
+ Returns:
+ The styled code snippet.
+ """
+ # For Python snippets, lint the code with black.
+ if language == "python":
+ code = ruff_format.format_string(textwrap.dedent(code)).strip()
+
+ # If needed, only display a subset of the lines.
+ if lines is not None:
+ code = textwrap.dedent(
+ "\n".join(code.strip().splitlines()[lines[0] : lines[1]])
+ ).strip()
+
+ # Create the code snippet.
+ cb = code_block_dark if theme == "dark" else code_block
+ return cb(
+ code=code,
+ language=language,
+ )
+
+
+def docdemo(
+ code: str,
+ state: str | None = None,
+ comp: rx.Component | None = None,
+ context: bool = False,
+ demobox_props: dict[str, Any] | None = None,
+ theme: str | None = None,
+ **props,
+) -> rx.Component:
+ """Create a documentation demo with code and output.
+
+ Args:
+ code: The code to render the component.
+ state: Code for any state needed for the component.
+ comp: The pre-rendered component.
+ context: Whether to wrap the render code in a function.
+ demobox_props: Props to apply to the demo box.
+ theme: The theme for the code snippet.
+ props: Additional props to apply to the component.
+
+ Returns:
+ The styled demo.
+ """
+ demobox_props = demobox_props or {}
+ # Render the component if necessary.
+ if comp is None:
+ comp = eval(code)
+
+ # Wrap the render code in a function if needed.
+ if context:
+ code = f"""def index():
+ return {code}
+ """
+
+ # Add the state code
+ if state is not None:
+ code = state + code
+
+ if demobox_props.pop("toggle", False):
+ return rx.tabs.root(
+ rx.tabs.list(
+ rx.tabs.trigger(
+ rx.box(
+ "UI",
+ ),
+ value="tab1",
+ class_name="tab-style",
+ ),
+ rx.tabs.trigger(
+ rx.box(
+ "Code",
+ ),
+ value="tab2",
+ class_name="tab-style",
+ ),
+ class_name="justify-end",
+ ),
+ rx.tabs.content(
+ rx.box(docdemobox(comp, **(demobox_props or {})), class_name="my-4"),
+ value="tab1",
+ ),
+ rx.tabs.content(
+ rx.box(doccode(code, theme=theme or "light"), class_name="my-4"),
+ value="tab2",
+ ),
+ default_value="tab1",
+ )
+ # Create the demo.
+ return rx.box(
+ docdemobox(comp, **(demobox_props or {})),
+ doccode(code, theme=theme or "light"),
+ class_name="py-4 gap-4 flex flex-col w-full",
+ **props,
+ )
+
+
+def docgraphing(
+ code: str,
+ comp: rx.Component | None = None,
+ data: str | None = None,
+):
+ return rx.box(
+ rx.flex(
+ comp,
+ class_name="w-full flex flex-col p-6 rounded-xl overflow-x-auto border border-slate-4 bg-slate-2 items-center justify-center",
+ ),
+ rx.tabs.root(
+ rx.tabs.list(
+ rx.tabs.trigger("Code", value="code", class_name="tab-style"),
+ rx.tabs.trigger("Data", value="data", class_name="tab-style"),
+ justify_content="end",
+ ),
+ rx.box(
+ rx.tabs.content(doccode(code), value="code", class_name="w-full px-0"),
+ rx.tabs.content(
+ doccode(data or ""), value="data", class_name="w-full px-0"
+ ),
+ class_name="w-full my-4",
+ ),
+ default_value="code",
+ class_name="w-full mt-6 justify-end",
+ ),
+ class_name="w-full py-4 flex flex-col",
+ )
diff --git a/shared/components/blocks/flexdown.py b/shared/components/blocks/flexdown.py
new file mode 100644
index 0000000..14d3a32
--- /dev/null
+++ b/shared/components/blocks/flexdown.py
@@ -0,0 +1,718 @@
+# pyright: reportAttributeAccessIssue=false
+import flexdown
+import reflex as rx
+from reflex_core.constants.colors import ColorType
+
+import reflex_ui as ui
+from shared.components.blocks.code import code_block_markdown, code_block_markdown_dark
+from shared.components.blocks.collapsible import collapsible_box
+from shared.components.blocks.demo import docdemo, docdemobox, docgraphing
+from shared.components.blocks.headings import (
+ h1_comp_xd,
+ h2_comp_xd,
+ h3_comp_xd,
+ h4_comp_xd,
+ img_comp_xd,
+)
+from shared.components.blocks.typography import (
+ code_comp,
+ definition,
+ doclink2,
+ list_comp,
+ ordered_list_comp,
+ text_comp,
+ unordered_list_comp,
+)
+from shared.constants import REFLEX_ASSETS_CDN
+from shared.styles.colors import c_color
+from shared.styles.fonts import base, code
+
+
+def get_code_style(color: ColorType):
+ return {
+ "p": {"margin_y": "0px"},
+ "code": {
+ "color": rx.color(color, 11),
+ "border_radius": "4px",
+ "border": f"1px solid {rx.color(color, 5)}",
+ "background": rx.color(color, 4),
+ **code,
+ },
+ **base,
+ }
+
+
+class AlertBlock(flexdown.blocks.MarkdownBlock):
+ """A block that displays a component along with its code."""
+
+ starting_indicator = "```md alert"
+ ending_indicator = "```"
+
+ include_indicators = True
+
+ def render(self, env: dict) -> rx.Component:
+ lines = self.get_lines(env)
+
+ args = lines[0].removeprefix(self.starting_indicator).split()
+
+ if len(args) == 0:
+ args = ["info"]
+ status = args[0]
+
+ if lines[1].startswith("#"):
+ title = lines[1].strip("#").strip()
+ content = "\n".join(lines[2:-1])
+ else:
+ title = ""
+ content = "\n".join(lines[1:-1])
+
+ colors: dict[str, ColorType] = {
+ "info": "accent",
+ "success": "grass",
+ "warning": "amber",
+ "error": "red",
+ }
+
+ color: ColorType = colors.get(status, "blue")
+
+ has_content = bool(content.strip())
+
+ icon = rx.box(
+ rx.match(
+ status,
+ ("info", rx.icon(tag="info", size=18, margin_right=".5em")),
+ ("success", rx.icon(tag="circle_check", size=18, margin_right=".5em")),
+ (
+ "warning",
+ rx.icon(tag="triangle_alert", size=18, margin_right=".5em"),
+ ),
+ ("error", rx.icon(tag="ban", size=18, margin_right=".5em")),
+ ),
+ color=f"{rx.color(color, 11)}",
+ )
+ title_comp = (
+ markdown_with_shiki(
+ title,
+ margin_y="0px",
+ style=get_code_style(color),
+ )
+ if title
+ else self.render_fn(content=content)
+ )
+
+ if has_content:
+ body = (
+ rx.accordion.content(
+ markdown(content), padding="0px", margin_top="16px"
+ )
+ if title
+ else rx.fragment()
+ )
+ return collapsible_box([icon, title_comp], body, color)
+
+ return rx.vstack(
+ rx.hstack(
+ icon,
+ markdown_with_shiki(
+ title,
+ color=f"{rx.color(color, 11)}",
+ margin_y="0px",
+ style=get_code_style(color),
+ ),
+ align_items="center",
+ width="100%",
+ spacing="1",
+ padding=["16px", "24px"],
+ ),
+ border=f"1px solid {rx.color(color, 4)}",
+ background_color=f"{rx.color(color, 3)}",
+ border_radius="12px",
+ margin_bottom="16px",
+ margin_top="16px",
+ width="100%",
+ )
+
+
+class SectionBlock(flexdown.blocks.Block):
+ """A block that displays a component along with its code."""
+
+ starting_indicator = "```md section"
+ ending_indicator = "```"
+
+ def render(self, env: dict) -> rx.Component:
+ lines = self.get_lines(env)
+
+ # Split up content into sections based on markdown headers.
+ header_indices = [i for i, line in enumerate(lines) if line.startswith("#")]
+ header_indices.append(len(lines))
+ sections = [
+ (
+ lines[header_indices[i]].strip("#"),
+ "\n".join(lines[header_indices[i] + 1 : header_indices[i + 1]]),
+ )
+ for i in range(len(header_indices) - 1)
+ ]
+
+ return rx.box(
+ rx.vstack(
+ *[
+ rx.fragment(
+ rx.text(
+ rx.text.span(
+ header,
+ font_weight="bold",
+ ),
+ width="100%",
+ ),
+ rx.box(
+ markdown(section),
+ width="100%",
+ ),
+ )
+ for header, section in sections
+ ],
+ text_align="left",
+ margin_y="1em",
+ width="100%",
+ ),
+ border_left=f"1.5px {c_color('slate', 4)} solid",
+ padding_left="1em",
+ width="100%",
+ align_items="center",
+ )
+
+
+class DefinitionBlock(flexdown.blocks.Block):
+ starting_indicator = "```md definition"
+ ending_indicator = "```"
+
+ def render(self, env: dict) -> rx.Component:
+ lines = self.get_lines(env)
+
+ # Split up content into sections based on markdown headers.
+ header_indices = [i for i, line in enumerate(lines) if line.startswith("#")]
+ header_indices.append(len(lines))
+ sections = [
+ (
+ lines[header_indices[i]].removeprefix("#"),
+ "\n".join(lines[header_indices[i] + 1 : header_indices[i + 1]]),
+ )
+ for i in range(len(header_indices) - 1)
+ ]
+
+ defs = [definition(title, content) for title, content in sections]
+
+ return rx.fragment(
+ rx.mobile_only(rx.vstack(*defs)),
+ rx.tablet_and_desktop(
+ rx.grid(
+ *[rx.box(d) for d in defs],
+ columns="2",
+ width="100%",
+ gap="1rem",
+ margin_bottom="1em",
+ )
+ ),
+ )
+
+
+class DemoOnly(flexdown.blocks.Block):
+ """A block that displays only a component demo without showing the code."""
+
+ starting_indicator = "```python demo-only"
+ ending_indicator = "```"
+ include_indicators = True
+ theme: str | None = None
+
+ def render(self, env: dict) -> rx.Component:
+ lines = self.get_lines(env)
+ code = "\n".join(lines[1:-1])
+
+ args = lines[0].removeprefix(self.starting_indicator).split()
+
+ exec_mode = env.get("__exec", False)
+ comp: rx.Component = rx.fragment()
+
+ for arg in args:
+ if arg.startswith("id="):
+ comp_id = arg.rsplit("id=")[-1]
+ break
+ else:
+ comp_id = None
+
+ if "exec" in args:
+ env["__xd"].exec(code, env, self.filename)
+ if not exec_mode:
+ comp = env[list(env.keys())[-1]]()
+ elif "graphing" in args:
+ env["__xd"].exec(code, env, self.filename)
+ if not exec_mode:
+ comp = env[list(env.keys())[-1]]()
+ # Get all the code before the final "def".
+ parts = code.rpartition("def")
+ data, code = parts[0], parts[1] + parts[2]
+ return docgraphing(code, comp=comp, data=data)
+ elif exec_mode:
+ return comp
+ elif "box" in args:
+ comp = eval(code, env, env)
+ return rx.box(comp, margin_bottom="1em", id=comp_id)
+ else:
+ comp = eval(code, env, env)
+
+ # Return only the component without any code display
+ return rx.box(comp, margin_bottom="1em", id=comp_id)
+
+
+class DemoBlock(flexdown.blocks.Block):
+ """A block that displays a component along with its code."""
+
+ starting_indicator = "```python demo"
+ ending_indicator = "```"
+ include_indicators = True
+ theme: str | None = None
+
+ def render(self, env: dict) -> rx.Component:
+ lines = self.get_lines(env)
+ code = "\n".join(lines[1:-1])
+
+ args = lines[0].removeprefix(self.starting_indicator).split()
+
+ exec_mode = env.get("__exec", False)
+ comp: rx.Component = rx.fragment()
+
+ for arg in args:
+ if arg.startswith("id="):
+ comp_id = arg.rsplit("id=")[-1]
+ break
+ else:
+ comp_id = None
+
+ if "exec" in args:
+ env["__xd"].exec(code, env, self.filename)
+ if not exec_mode:
+ comp = env[list(env.keys())[-1]]()
+ elif "graphing" in args:
+ env["__xd"].exec(code, env, self.filename)
+ if not exec_mode:
+ comp = env[list(env.keys())[-1]]()
+ # Get all the code before the final "def".
+ parts = code.rpartition("def")
+ data, code = parts[0], parts[1] + parts[2]
+ return docgraphing(code, comp=comp, data=data)
+ elif exec_mode:
+ return comp
+ elif "box" in args:
+ comp = eval(code, env, env)
+ return rx.box(docdemobox(comp), margin_bottom="1em", id=comp_id)
+ else:
+ comp = eval(code, env, env)
+
+ # Sweep up additional CSS-like props to apply to the demobox itself
+ demobox_props = {}
+ for arg in args:
+ prop, equals, value = arg.partition("=")
+ if equals:
+ demobox_props[prop] = value
+
+ if "toggle" in args:
+ demobox_props["toggle"] = True
+
+ return docdemo(
+ code, comp=comp, demobox_props=demobox_props, theme=self.theme, id=comp_id
+ )
+
+
+class DemoBlockDark(DemoBlock):
+ theme = "dark"
+
+
+class DemoBlockNestedMarkdown(DemoBlock):
+ """Used when the block contains literal markdown with triple backticks."""
+
+ starting_indicator = "````python demo"
+ ending_indicator = "````"
+
+
+class DemoBlockNestedMarkdownDark(DemoBlockNestedMarkdown):
+ theme = "dark"
+
+
+class VideoBlock(flexdown.blocks.MarkdownBlock):
+ """A block that displays a video."""
+
+ starting_indicator = "```md video"
+ ending_indicator = "```"
+
+ include_indicators = True
+
+ def render(self, env: dict) -> rx.Component:
+ lines = self.get_lines(env)
+
+ args = lines[0].removeprefix(self.starting_indicator).split()
+
+ if len(args) == 0:
+ args = ["info"]
+ url = args[0]
+
+ title = lines[1].strip("#").strip() if lines[1].startswith("#") else ""
+
+ color: ColorType = "blue"
+
+ trigger = [
+ markdown_with_shiki(
+ title or "Video Description",
+ margin_y="0px",
+ style=get_code_style(color),
+ ),
+ ]
+ body = rx.accordion.content(
+ rx.video(
+ src=url,
+ width="100%",
+ height="500px",
+ border_radius="10px",
+ overflow="hidden",
+ ),
+ margin_top="16px",
+ padding="0px",
+ )
+ return collapsible_box(trigger, body, color, item_border_radius="0px")
+
+
+class QuoteBlock(flexdown.blocks.MarkdownBlock):
+ """A block that displays a quote."""
+
+ starting_indicator = "```md quote"
+ ending_indicator = "```"
+
+ include_indicators = True
+
+ def _parse(self, env: dict) -> dict[str, str]:
+ lines = self.get_lines(env)
+ quote_content = []
+ data = {
+ "name": "",
+ "role": "",
+ "image": "",
+ "variant": "small",
+ }
+
+ for line in lines[1:-1]: # Skip the first and last lines (indicators)
+ if line.startswith("- name:"):
+ data["name"] = line.split(":", 1)[1].strip()
+ elif line.startswith("- role:"):
+ data["role"] = line.split(":", 1)[1].strip()
+ elif line.startswith("- image:"):
+ data["image"] = line.split(":", 1)[1].strip()
+ elif line.startswith("- variant:"):
+ data["variant"] = line.split(":", 1)[1].strip().lower()
+ else:
+ quote_content.append(line)
+
+ data["quote_text"] = "\n".join(quote_content).strip()
+ return data
+
+ def _author(self, name: str, role: str, class_name: str = "") -> rx.Component:
+ return rx.el.div(
+ rx.el.span(
+ name,
+ class_name="text-xs font-mono uppercase font-[415] text-secondary-12",
+ ),
+ rx.el.span(
+ role,
+ class_name="text-xs font-mono font-[415] text-secondary-11 uppercase",
+ ),
+ class_name=ui.cn("flex flex-col gap-0.5", class_name),
+ )
+
+ def _avatar(
+ self, name: str, image: str, class_name: str = ""
+ ) -> rx.Component | None:
+ if not image:
+ return None
+ avatar_class = ui.cn("rounded-full object-cover aspect-square", class_name)
+ return rx.image(
+ src=f"{REFLEX_ASSETS_CDN}case_studies/people/{image}",
+ alt=f"{name} profile picture",
+ class_name=avatar_class,
+ )
+
+ def _render_medium(self, data: dict[str, str]) -> rx.Component:
+ return rx.el.div(
+ rx.el.div(
+ self._avatar(data["name"], data["image"], class_name="size-6"),
+ class_name="p-4 shrink-0 lg:border-r border-secondary-8 border-dashed max-lg:border-b bg-secondary-1",
+ ),
+ rx.el.span(
+ f'"{data["quote_text"]}"',
+ class_name="text-secondary-12 text-base font-[575] p-4 bg-white-1 w-full",
+ ),
+ class_name="flex lg:flex-row flex-col border border-dashed border-secondary-8 mt-2 mb-6 rounded-lg overflow-hidden box-border bg-white-1",
+ )
+
+ def _render_small(self, data: dict[str, str]) -> rx.Component:
+ return rx.el.div(
+ rx.el.span(
+ f'"{data["quote_text"]}"',
+ class_name="text-secondary-12 text-lg font-[575] p-6 lg:border-r border-secondary-8 border-dashed max-lg:border-b bg-white-1",
+ ),
+ rx.el.div(
+ rx.el.div(
+ self._author(data["name"], data["role"]),
+ class_name="text-end text-nowrap",
+ ),
+ self._avatar(data["name"], data["image"], class_name="size-14"),
+ class_name="flex flex-row gap-6 items-center p-6 shrink-0 bg-secondary-1",
+ ),
+ class_name="flex lg:flex-row flex-col border border-dashed border-secondary-8 mt-2 mb-6 rounded-lg overflow-hidden box-border bg-white-1",
+ )
+
+ def _render_big(self, data: dict[str, str]) -> rx.Component:
+ return rx.el.div(
+ rx.el.div(
+ rx.el.span(
+ f"{data['quote_text']}",
+ class_name="text-secondary-12 text-2xl font-[575]",
+ ),
+ rx.el.div(
+ self._avatar(data["name"], data["image"], class_name="size-6"),
+ self._author(
+ data["name"],
+ data["role"],
+ class_name="flex-row gap-3.5 items-center",
+ ),
+ class_name="flex flex-row gap-3.5 items-center",
+ ),
+ class_name="flex flex-col gap-12 pr-[12.5rem] relative z-10",
+ ),
+ rx.image(
+ src=f"{REFLEX_ASSETS_CDN}common/{rx.color_mode_cond('light', 'dark')}/quote_squares.svg",
+ loading="lazy",
+ alt="Quote icon",
+ class_name="absolute right-0 inset-y-0 h-[calc(100%)] min-h-full w-auto origin-right pointer-events-none object-contain object-right",
+ ),
+ class_name="flex flex-col dark:border bg-white-1 dark:border-secondary-4 mt-2 mb-6 overflow-hidden shadow-[0_0_0_1px_rgba(0,0,0,0.12)_inset,0_6px_12px_0_rgba(0,0,0,0.06),0_1px_1px_0_rgba(0,0,0,0.01),0_4px_6px_0_rgba(0,0,0,0.02)] rounded-xl py-8 px-8 relative",
+ )
+
+ def render(self, env: dict) -> rx.Component:
+ data = self._parse(env)
+ renderers = {
+ "small": self._render_small,
+ "medium": self._render_medium,
+ "big": self._render_big,
+ }
+ renderer = renderers.get(data["variant"], self._render_small)
+ return renderer(data)
+
+
+class TabsBlock(flexdown.blocks.Block):
+ """A block that displays content in tabs."""
+
+ starting_indicator = "---md tabs"
+ ending_indicator = "---"
+
+ def render(self, env: dict) -> rx.Component:
+ lines = self.get_lines(env)
+
+ tab_sections = []
+ current_section = []
+ current_title = ""
+
+ for line in lines[1:-1]: # Skip the first and last lines (indicators)
+ stripped_line = line.strip()
+
+ if stripped_line.startswith("--tab "):
+ if current_title:
+ tab_sections.append((current_title, "\n".join(current_section)))
+ current_title = stripped_line[6:].strip()
+ current_section = []
+ elif stripped_line == "--":
+ if current_title:
+ tab_sections.append((current_title, "\n".join(current_section)))
+ current_title = ""
+ current_section = []
+ else:
+ current_section.append(line)
+
+ # Add the last section if there's content
+ if current_title and current_section:
+ tab_sections.append((current_title, "\n".join(current_section)))
+
+ # Create tab components
+ triggers = []
+ contents = []
+
+ for i, (title, content) in enumerate(tab_sections):
+ value = f"tab{i + 1}"
+ triggers.append(
+ rx.tabs.trigger(
+ title,
+ value=value,
+ class_name="tab-style font-base font-semibold text-[1.25rem]",
+ )
+ )
+
+ # Render the tab content
+ tab_content = []
+ for block in env["__xd"].get_blocks(content, self.filename):
+ if isinstance(block, flexdown.blocks.MarkdownBlock):
+ block.render_fn = env["__xd"].flexdown_memo
+ try:
+ tab_content.append(block.render(env=env))
+ except Exception:
+ print(
+ f"Error while rendering {type(block)} on line {block.start_line_number}. "
+ f"\n{block.get_content(env)}"
+ )
+ raise
+
+ contents.append(rx.tabs.content(rx.fragment(*tab_content), value=value))
+
+ return rx.tabs.root(
+ rx.tabs.list(*triggers, class_name="mt-4"), *contents, default_value="tab1"
+ )
+
+
+def _markdown_table(*children, **props) -> rx.Component:
+ return rx.box(
+ rx.el.table(
+ *children,
+ class_name="w-full border-collapse text-sm border border-secondary-4 rounded-lg overflow-hidden bg-white-1 ",
+ **props,
+ ),
+ class_name="w-full rounded-xl border border-secondary-a4 my-6 max-w-full overflow-hidden",
+ )
+
+
+def _markdown_thead(*children, **props) -> rx.Component:
+ return rx.el.thead(
+ *children,
+ class_name="bg-secondary-1 border-b border-secondary-4",
+ **props,
+ )
+
+
+def _markdown_tbody(*children, **props) -> rx.Component:
+ return rx.el.tbody(
+ *children,
+ class_name="[&_tr:nth-child(even)]:bg-secondary-1",
+ **props,
+ )
+
+
+def _markdown_tr(*children, **props) -> rx.Component:
+ return rx.el.tr(
+ *children,
+ class_name="border-b border-secondary-4 last:border-b-0",
+ **props,
+ )
+
+
+def _markdown_th(*children, **props) -> rx.Component:
+ return rx.el.th(
+ *children,
+ class_name="px-3 py-2.5 text-left text-xs font-[575] text-secondary-12 align-top",
+ **props,
+ )
+
+
+def _markdown_td(*children, **props) -> rx.Component:
+ return rx.el.td(
+ *children,
+ class_name="px-3 py-2.5 text-xs font-medium first:font-[575] text-secondary-11 align-top",
+ **props,
+ )
+
+
+_markdown_table_component_map: dict[str, object] = {
+ "table": _markdown_table,
+ "thead": _markdown_thead,
+ "tbody": _markdown_tbody,
+ "tr": _markdown_tr,
+ "th": _markdown_th,
+ "td": _markdown_td,
+}
+
+component_map = {
+ "h1": lambda text: h1_comp_xd(text=text),
+ "h2": lambda text: h2_comp_xd(text=text),
+ "h3": lambda text: h3_comp_xd(text=text),
+ "h4": lambda text: h4_comp_xd(text=text),
+ "p": lambda text: text_comp(text=text),
+ "li": lambda text: list_comp(text=text),
+ "a": doclink2,
+ "code": lambda text: code_comp(text=text),
+ "pre": code_block_markdown,
+ "img": lambda src: img_comp_xd(src=src),
+ **_markdown_table_component_map,
+}
+comp2 = component_map.copy()
+comp2["pre"] = code_block_markdown_dark
+comp2["ul"] = lambda items: unordered_list_comp(items=items)
+comp2["ol"] = lambda items: ordered_list_comp(items=items)
+
+
+xd = flexdown.Flexdown(
+ block_types=[
+ DemoOnly,
+ DemoBlock,
+ DemoBlockNestedMarkdown,
+ AlertBlock,
+ DefinitionBlock,
+ SectionBlock,
+ VideoBlock,
+ TabsBlock,
+ QuoteBlock,
+ ],
+ component_map=component_map,
+)
+xd.clear_modules()
+xd2 = flexdown.Flexdown(
+ block_types=[
+ DemoBlockDark,
+ DemoBlockNestedMarkdownDark,
+ AlertBlock,
+ DefinitionBlock,
+ SectionBlock,
+ VideoBlock,
+ TabsBlock,
+ QuoteBlock,
+ ],
+ component_map=comp2,
+)
+xd2.clear_modules()
+
+
+def markdown(text: str):
+ return xd.get_default_block().render_fn(content=text)
+
+
+def markdown_codeblock(value: str, **props: object) -> rx.Component:
+ """Render a code block using the Shiki-based code block component."""
+ return rx._x.code_block(value, **props)
+
+
+def markdown_with_shiki(*args, **kwargs):
+ """Wrapper for the markdown component with a customized component map.
+ Uses the experimental Shiki-based code block (rx._x.code_block)
+ instead of the default CodeBlock component for code blocks.
+
+ Note: This wrapper should be removed once the default codeblock
+ in rx.markdown component map is updated to the Shiki-based code block.
+ """
+ return rx.markdown(
+ *args,
+ component_map={
+ "h1": lambda text: h1_comp_xd(text=text),
+ "h2": lambda text: h2_comp_xd(text=text),
+ "h3": lambda text: h3_comp_xd(text=text),
+ "h4": lambda text: h4_comp_xd(text=text),
+ "p": lambda text: text_comp(text=text),
+ "li": lambda text: list_comp(text=text),
+ "a": doclink2,
+ "pre": markdown_codeblock,
+ "img": lambda src: img_comp_xd(src=src),
+ },
+ **kwargs,
+ )
diff --git a/shared/components/blocks/headings.py b/shared/components/blocks/headings.py
new file mode 100644
index 0000000..19e55d0
--- /dev/null
+++ b/shared/components/blocks/headings.py
@@ -0,0 +1,206 @@
+# pyright: reportArgumentType=false, reportReturnType=false, reportOperatorIssue=false
+"""Template for documentation pages."""
+
+from typing import ClassVar
+
+import reflex as rx
+
+from shared.views.hosting_banner import HostingBannerState
+
+icon_margins = {
+ "h1": "10px",
+ "h2": "5px",
+ "h3": "2px",
+ "h4": "0px",
+}
+
+
+class HeadingLink(rx.link.__self__):
+ # This function is imported from 'hast-util-to-string' package.
+ HAST_NODE_TO_STRING: ClassVar = rx.vars.FunctionStringVar(
+ _js_expr="hastNodeToString",
+ )
+
+ # This function is defined by add_custom_code.
+ SLUGIFY_MIXED_TEXT_HAST_NODE: ClassVar = rx.vars.FunctionStringVar(
+ _js_expr="slugifyMixedTextHastNode",
+ )
+
+ def add_custom_code(self) -> list[rx.Var]:
+ def node_to_string(node: rx.Var) -> rx.vars.StringVar:
+ return rx.cond(
+ node.js_type() == "string",
+ node,
+ rx.cond(
+ (node.js_type() == "object")
+ & node.to(dict)["props"].to(dict)["node"],
+ self.HAST_NODE_TO_STRING(node.to(dict)["props"].to(dict)["node"]),
+ "object",
+ ),
+ ).to(str)
+
+ def slugify(node: rx.Var) -> rx.vars.StringVar:
+ return (
+ rx.cond(
+ rx.vars.function.ARRAY_ISARRAY(node),
+ rx.vars.sequence.map_array_operation(
+ node,
+ rx.vars.function.ArgsFunctionOperation.create(
+ args_names=["childNode"],
+ return_expr=node_to_string(rx.vars.Var("childNode")),
+ ),
+ ).join("-"),
+ node_to_string(node),
+ )
+ .to(str)
+ .lower()
+ .split(" ")
+ .join("-")
+ )
+
+ return [
+ f"const {self.SLUGIFY_MIXED_TEXT_HAST_NODE!s} = "
+ + str(
+ rx.vars.function.ArgsFunctionOperation.create(
+ args_names=["givenNode"],
+ return_expr=slugify(rx.vars.Var("givenNode")),
+ )
+ )
+ ]
+
+ def add_imports(self) -> dict[str, list[rx.ImportVar]]:
+ return {
+ "hast-util-to-string@3.0.1": [
+ rx.ImportVar(tag="toString", alias="hastNodeToString", is_default=False)
+ ],
+ }
+
+ @classmethod
+ def slugify(cls, node: rx.Var) -> rx.vars.StringVar:
+ return cls.SLUGIFY_MIXED_TEXT_HAST_NODE(node).to(str)
+
+ @classmethod
+ def create(
+ cls,
+ text: str,
+ heading: str,
+ style: dict | None = None,
+ mt: str = "4",
+ class_name: str = "",
+ ) -> rx.Component:
+ id_ = cls.slugify(text)
+ href = rx.State.router.page.full_path + "#" + id_
+ scroll_margin = rx.cond(
+ HostingBannerState.is_banner_visible,
+ "scroll-mt-[113px]",
+ "scroll-mt-[77px]",
+ )
+
+ return super().create(
+ rx.heading(
+ text,
+ id=id_,
+ as_=heading,
+ style=style if style is not None else {},
+ class_name=class_name + " " + scroll_margin + " mt-" + mt,
+ ),
+ rx.icon(
+ tag="link",
+ size=18,
+ class_name="!text-violet-11 invisible transition-[visibility_0.075s_ease-out] group-hover:visible mt-"
+ + mt,
+ ),
+ underline="none",
+ href=href,
+ on_click=lambda: rx.set_clipboard(href),
+ class_name="flex flex-row items-center gap-6 hover:!text-violet-11 cursor-pointer mb-6 transition-colors group text-m-slate-12 dark:text-m-slate-3 ",
+ )
+
+
+h_comp_common = HeadingLink.create
+
+
+@rx.memo
+def h1_comp(text: str) -> rx.Component:
+ return h_comp_common(
+ text=text,
+ heading="h1",
+ class_name="lg:text-5xl text-3xl font-[525]",
+ )
+
+
+@rx.memo
+def h1_comp_xd(text: str) -> rx.Component:
+ return h_comp_common(
+ text=text,
+ heading="h1",
+ class_name="lg:text-5xl text-3xl font-[525]",
+ )
+
+
+@rx.memo
+def h2_comp(text: str) -> rx.Component:
+ return h_comp_common(
+ text=text,
+ heading="h2",
+ mt="8",
+ class_name="lg:text-4xl text-2xl font-[525]",
+ )
+
+
+@rx.memo
+def h2_comp_xd(text: str) -> rx.Component:
+ return h_comp_common(
+ text=text,
+ heading="h2",
+ mt="8",
+ class_name="lg:text-3xl text-2xl font-[525]",
+ )
+
+
+@rx.memo
+def h3_comp(text: str) -> rx.Component:
+ return h_comp_common(
+ text=text,
+ heading="h3",
+ mt="4",
+ class_name="lg:text-2xl text-xl font-[525]",
+ )
+
+
+@rx.memo
+def h3_comp_xd(text: str) -> rx.Component:
+ return h_comp_common(
+ text=text,
+ heading="h3",
+ mt="4",
+ class_name="lg:text-2xl text-lg font-[525]",
+ )
+
+
+@rx.memo
+def h4_comp(text: str) -> rx.Component:
+ return h_comp_common(
+ text=text,
+ heading="h4",
+ mt="2",
+ class_name="lg:text-xl text-lg font-[525]",
+ )
+
+
+@rx.memo
+def h4_comp_xd(text: str) -> rx.Component:
+ return h_comp_common(
+ text=text,
+ heading="h4",
+ mt="2",
+ class_name="lg:text-xl text-lg font-[525]",
+ )
+
+
+@rx.memo
+def img_comp_xd(src: str) -> rx.Component:
+ return rx.image(
+ src=src,
+ class_name="rounded-lg border border-secondary-a4 mb-2",
+ )
diff --git a/shared/components/blocks/typography.py b/shared/components/blocks/typography.py
new file mode 100644
index 0000000..ee54fe8
--- /dev/null
+++ b/shared/components/blocks/typography.py
@@ -0,0 +1,106 @@
+"""Typography blocks for doc pages."""
+
+import reflex as rx
+
+from shared.styles import fonts
+
+
+def definition(title: str, *children) -> rx.Component:
+ """Create a definition for a doc page.
+
+ Args:
+ title: The title of the definition.
+ children: The children to display.
+
+ Returns:
+ The styled definition.
+ """
+ return rx.vstack(
+ rx.heading(
+ title, font_size="1em", font_weight="bold", color=rx.color("mauve", 12)
+ ),
+ *children,
+ color=rx.color("mauve", 10),
+ padding="1em",
+ border=f"1px solid {rx.color('mauve', 4)}",
+ background_color=rx.color("mauve", 2),
+ border_radius="8px",
+ _hover={
+ "border": f"1px solid {rx.color('mauve', 5)}",
+ "background_color": rx.color("mauve", 3),
+ },
+ align_items="start",
+ )
+
+
+@rx.memo
+def text_comp(text: rx.Var[str]) -> rx.Component:
+ return rx.text(text, class_name="font-[475] text-secondary-11 mb-4 leading-7")
+
+
+@rx.memo
+def text_comp_2(text: rx.Var[str]) -> rx.Component:
+ return rx.text(
+ text,
+ class_name="font-[475] text-secondary-11 max-w-[80%] mb-10",
+ )
+
+
+@rx.memo
+def list_comp(text: rx.Var[str]) -> rx.Component:
+ return rx.list_item(text, class_name="font-[475] text-secondary-11 mb-4")
+
+
+@rx.memo
+def unordered_list_comp(items: rx.Var[list[str]]) -> rx.Component:
+ return rx.list.unordered(items, class_name="mb-6")
+
+
+@rx.memo
+def ordered_list_comp(items: rx.Var[list[str]]) -> rx.Component:
+ return rx.list.ordered(items, class_name="mb-6")
+
+
+@rx.memo
+def code_comp(text: rx.Var[str]) -> rx.Component:
+ return rx.code(text, class_name="code-style")
+
+
+def doclink(text: str, href: str, **props) -> rx.Component:
+ """Create a styled link for doc pages.
+
+ Args:
+ text: The text to display.
+ href: The link to go to.
+ props: Props to apply to the link.
+
+ Returns:
+ The styled link.
+ """
+ return rx.link(
+ text,
+ underline="always",
+ href=href,
+ **props,
+ class_name="!text-m-slate-12 dark:!text-m-slate-3 !decoration-m-slate-12 dark:!decoration-m-slate-3",
+ )
+
+
+def doclink2(text: str, **props) -> rx.Component:
+ """Create a styled link for doc pages.
+
+ Args:
+ text: The text to display.
+ href: The link to go to.
+ props: Props to apply to the link.
+
+ Returns:
+ The styled link.
+ """
+ return rx.link(
+ text,
+ underline="always",
+ **props,
+ style=fonts.base,
+ class_name="!text-m-slate-12 dark:!text-m-slate-3 !decoration-m-slate-12 dark:!decoration-m-slate-3",
+ )
diff --git a/shared/components/code_card.py b/shared/components/code_card.py
new file mode 100644
index 0000000..f9cb3d8
--- /dev/null
+++ b/shared/components/code_card.py
@@ -0,0 +1,184 @@
+import re
+
+import reflex as rx
+from reflex.experimental.client_state import ClientStateVar
+
+import reflex_ui as ui
+from shared.components.icons import get_icon
+
+
+@rx.memo
+def install_command(
+ command: str,
+ show_dollar_sign: bool = True,
+) -> rx.Component:
+ copied = ClientStateVar.create("is_copied", default=False, global_ref=False)
+ return rx.el.button(
+ rx.cond(
+ copied.value,
+ ui.icon(
+ "Tick02Icon",
+ size=14,
+ class_name="ml-[5px] shrink-0",
+ ),
+ ui.icon("Copy01Icon", size=14, class_name="shrink-0 ml-[5px]"),
+ ),
+ rx.text(
+ rx.cond(
+ show_dollar_sign,
+ f"${command}",
+ command,
+ ),
+ as_="p",
+ class_name="font-small text-start truncate",
+ ),
+ title=command,
+ on_click=[
+ rx.call_function(copied.set_value(True)),
+ rx.set_clipboard(command),
+ ],
+ on_mouse_down=rx.call_function(copied.set_value(False)).debounce(1500),
+ class_name="flex items-center gap-1.5 border-slate-5 bg-slate-1 hover:bg-slate-3 shadow-small pr-1.5 border rounded-md w-full text-slate-9 transition-bg cursor-pointer overflow-hidden min-w-0 flex-1 h-[24px]",
+ style={
+ "opacity": "1",
+ "cursor": "pointer",
+ "transition": "background 0.250s ease-out",
+ "&>svg": {
+ "transition": "transform 0.250s ease-out, opacity 0.250s ease-out",
+ },
+ },
+ )
+
+
+def repo(repo_url: str) -> rx.Component:
+ return rx.link(
+ get_icon(icon="new_tab", class_name="p-[5px]"),
+ href=repo_url,
+ is_external=True,
+ class_name="border-slate-5 bg-slate-1 hover:bg-slate-3 shadow-small border border-solid rounded-md text-slate-9 hover:!text-slate-9 no-underline transition-bg cursor-pointer shrink-0",
+ )
+
+
+def code_card(app: dict) -> rx.Component:
+ return rx.flex(
+ rx.box(
+ rx.el.a(
+ rx.image(
+ src=app["image_url"],
+ loading="lazy",
+ alt="Image preview for app: " + app["name"],
+ class_name="size-full duration-150 object-top object-cover hover:scale-105 transition-transform ease-out",
+ ),
+ href=app["demo_url"],
+ is_external=True,
+ ),
+ class_name="relative border-slate-5 border-b border-solid w-full overflow-hidden h-[180px]",
+ ),
+ rx.box(
+ rx.box(
+ rx.el.h4(
+ app["name"],
+ class_name="font-smbold text-slate-12 truncate",
+ ),
+ class_name="flex flex-row justify-between items-center gap-3 p-[0.625rem_0.75rem_0rem_0.75rem] w-full",
+ ),
+ rx.box(
+ install_command(
+ "reflex init --template " + app["demo_url"], show_dollar_sign=False
+ ),
+ rx.cond(app["source"], repo(app["source"])),
+ rx.link(
+ get_icon(icon="eye", class_name="p-[5px]"),
+ href=app["demo_url"],
+ is_external=True,
+ class_name="border-slate-5 bg-slate-1 hover:bg-slate-3 shadow-small border border-solid rounded-md text-slate-9 hover:!text-slate-9 no-underline transition-bg cursor-pointer",
+ ),
+ class_name="flex flex-row items-center gap-[6px] p-[0rem_0.375rem_0.375rem_0.375rem] w-full",
+ ),
+ class_name="flex flex-col gap-[10px] w-full",
+ ),
+ style={
+ "animation": "fade-in 0.35s ease-out",
+ "@keyframes fade-in": {
+ "0%": {"opacity": "0"},
+ "100%": {"opacity": "1"},
+ },
+ },
+ class_name="box-border flex flex-col border-slate-5 bg-slate-1 shadow-large border rounded-xl w-full h-[280px] overflow-hidden",
+ )
+
+
+def gallery_app_card(app: dict[str, str]) -> rx.Component:
+ slug = re.sub(r"[\s_]+", "-", app["title"]).lower()
+ return rx.flex(
+ rx.box(
+ rx.link(
+ rx.image(
+ src=app["image"],
+ loading="lazy",
+ alt="Image preview for app: " + app["title"],
+ class_name="size-full duration-150 object-cover hover:scale-105 transition-transform ease-out",
+ ),
+ href=f"/docs/getting-started/open-source-templates/{slug}",
+ ),
+ class_name="relative border-slate-5 border-b border-solid w-full overflow-hidden h-[180px]",
+ ),
+ rx.box(
+ rx.box(
+ rx.el.h6(
+ app["title"],
+ class_name="font-smbold text-slate-12 truncate shrink-0",
+ width="100%",
+ ),
+ rx.text(
+ app["description"],
+ class_name="text-slate-10 font-small truncate text-pretty shrink-0",
+ width="100%",
+ ),
+ rx.box(
+ rx.box(
+ install_command(
+ command=f"reflex init --template {app['title']}",
+ show_dollar_sign=False,
+ ),
+ *(
+ [
+ rx.box(
+ repo(app["demo"]),
+ class_name="flex flex-row justify-start",
+ )
+ ]
+ if "demo" in app
+ else []
+ ),
+ class_name="flex flex-row max-w-full gap-2 w-full shrink-0",
+ ),
+ rx.box(class_name="grow"),
+ rx.cond(
+ "Reflex" in app["author"],
+ rx.box(
+ rx.text(
+ "by",
+ class_name="text-slate-9 font-small",
+ ),
+ get_icon(icon="badge_logo"),
+ rx.text(
+ app["author"],
+ class_name="text-slate-9 font-small",
+ ),
+ class_name="flex flex-row items-start gap-1",
+ ),
+ rx.text(
+ f"by {app['author']}",
+ class_name="text-slate-9 font-small",
+ ),
+ ),
+ class_name="flex flex-col gap-[6px] size-full",
+ ),
+ class_name="flex flex-col items-start gap-2 p-[0.625rem_0.75rem_0.625rem_0.75rem] w-full h-full",
+ ),
+ class_name="flex flex-col gap-[10px] w-full h-full flex-1",
+ ),
+ key=app["title"],
+ class_name="box-border flex-col border-slate-5 bg-slate-1 shadow-large border rounded-xl w-full h-[360px] overflow-hidden",
+ )
diff --git a/shared/components/docs.py b/shared/components/docs.py
new file mode 100644
index 0000000..92cd688
--- /dev/null
+++ b/shared/components/docs.py
@@ -0,0 +1,186 @@
+"""Template for documentation pages."""
+
+from typing import Any
+
+import flexdown
+import mistletoe
+
+from .blocks import *
+
+
+def right_sidebar_item_highlight():
+ return r"""
+ function setupTableOfContentsHighlight() {
+ // Delay to ensure DOM is fully loaded
+ setTimeout(() => {
+ const tocLinks = document.querySelectorAll('#toc-navigation a');
+ const activeClasses = [
+ 'text-primary-9',
+ 'dark:text-primary-11',
+ 'shadow-[1.5px_0_0_0_var(--primary-11)_inset]',
+ 'dark:shadow-[1.5px_0_0_0_var(--primary-9)_inset]',
+ ];
+ const defaultClasses = ['text-m-slate-7', 'dark:text-m-slate-6'];
+
+ function normalizeId(id) {
+ return id.toLowerCase().replace(/\s+/g, '-');
+ }
+
+ function setDefaultState(link) {
+ activeClasses.forEach(cls => link.classList.remove(cls));
+ defaultClasses.forEach(cls => link.classList.add(cls));
+ }
+
+ function setActiveState(link) {
+ defaultClasses.forEach(cls => link.classList.remove(cls));
+ activeClasses.forEach(cls => link.classList.add(cls));
+ }
+
+ function highlightTocLink() {
+ // Get the current hash from the URL
+ const currentHash = window.location.hash.substring(1);
+
+ // Reset all links
+ tocLinks.forEach(link => setDefaultState(link));
+
+ // If there's a hash, find and highlight the corresponding link
+ if (currentHash) {
+ const correspondingLink = Array.from(tocLinks).find(link => {
+ // Extract the ID from the link's href
+ const linkHash = new URL(link.href).hash.substring(1);
+ return normalizeId(linkHash) === normalizeId(currentHash);
+ });
+
+ if (correspondingLink) {
+ setActiveState(correspondingLink);
+ }
+ }
+ }
+
+ // Add click event listeners to TOC links to force highlight
+ tocLinks.forEach(link => {
+ link.addEventListener('click', (e) => {
+ // Remove active class from all links
+ tocLinks.forEach(otherLink => setDefaultState(otherLink));
+
+ // Add active class to clicked link
+ setActiveState(e.target);
+ });
+ });
+
+ // Intersection Observer for scroll-based highlighting
+ const observerOptions = {
+ root: null,
+ rootMargin: '-20% 0px -70% 0px',
+ threshold: 0
+ };
+
+ const observer = new IntersectionObserver((entries) => {
+ entries.forEach(entry => {
+ if (entry.isIntersecting) {
+ const headerId = entry.target.id;
+
+ // Find corresponding TOC link
+ const correspondingLink = Array.from(tocLinks).find(link => {
+ const linkHash = new URL(link.href).hash.substring(1);
+ return normalizeId(linkHash) === normalizeId(headerId);
+ });
+
+ if (correspondingLink) {
+ // Reset all links
+ tocLinks.forEach(link => setDefaultState(link));
+
+ // Highlight current link
+ setActiveState(correspondingLink);
+ }
+ }
+ });
+ }, observerOptions);
+
+ // Observe headers
+ const headerSelectors = Array.from(tocLinks).map(link =>
+ new URL(link.href).hash.substring(1)
+ );
+
+ headerSelectors.forEach(selector => {
+ const header = document.getElementById(selector);
+ if (header) {
+ observer.observe(header);
+ }
+ });
+
+ // Initial highlighting
+ highlightTocLink();
+
+ // Handle hash changes
+ window.addEventListener('hashchange', highlightTocLink);
+ }, 100);
+}
+
+// Run the function when the page loads
+setupTableOfContentsHighlight();
+ """
+
+
+def get_headings(comp: Any):
+ """Get the strings from markdown component."""
+ if isinstance(comp, mistletoe.block_token.Heading):
+ heading_text = "".join(
+ token.content for token in comp.children if hasattr(token, "content")
+ )
+ return [(comp.level, heading_text)]
+
+ # Recursively get the strings from the children.
+ if not hasattr(comp, "children") or comp.children is None:
+ return []
+
+ headings = []
+ for child in comp.children:
+ headings.extend(get_headings(child))
+ return headings
+
+
+def get_toc(source: Any, href: str, component_list: list | None = None):
+ from shared.flexdown import xd
+
+ component_list = component_list or []
+ component_list = component_list[1:]
+
+ # Generate the TOC
+ # The environment used for execing and evaling code.
+ from shared.constants import REFLEX_ASSETS_CDN
+
+ env = source.metadata
+ env["__xd"] = xd
+ env["REFLEX_ASSETS_CDN"] = REFLEX_ASSETS_CDN
+
+ # Get the content of the document.
+ doc_content = source.content
+
+ # Get the blocks in the source code.
+ # Note: we must use reflex-web's special flexdown instance xd here - it knows about all custom block types (like DemoBlock)
+ blocks = xd.get_blocks(doc_content, href)
+
+ content_pieces = []
+ for block in blocks:
+ if (
+ not isinstance(block, flexdown.blocks.MarkdownBlock)
+ or len(block.lines) == 0
+ or not block.lines[0].startswith("#")
+ ):
+ continue
+ # Now we should have all the env entries we need
+ content = block.get_content(env)
+ content_pieces.append(content)
+
+ content = "\n".join(content_pieces)
+ doc = mistletoe.Document(content)
+
+ # Parse the markdown headers.
+ headings = get_headings(doc)
+
+ if len(component_list):
+ headings.append((1, "API Reference"))
+ for component_tuple in component_list:
+ headings.append((2, component_tuple[1]))
+ return headings, doc_content
diff --git a/shared/components/hosting_banner.py b/shared/components/hosting_banner.py
new file mode 100644
index 0000000..30a6c5b
--- /dev/null
+++ b/shared/components/hosting_banner.py
@@ -0,0 +1,5 @@
+"""Re-export hosting banner from shared.views.hosting_banner."""
+
+from shared.views.hosting_banner import HostingBannerState, hosting_banner
+
+__all__ = ["HostingBannerState", "hosting_banner"]
diff --git a/shared/components/icons.py b/shared/components/icons.py
new file mode 100644
index 0000000..a50a0ca
--- /dev/null
+++ b/shared/components/icons.py
@@ -0,0 +1,691 @@
+import reflex as rx
+
+github = """
+"""
+
+discord = """
+"""
+
+arrow_down = """
+"""
+
+arrow_down_big = """
+"""
+
+new_tab = """
+"""
+
+copy = """
+"""
+
+eye = """
+"""
+
+twitter = """
+"""
+
+moon = """
+"""
+
+sun = """