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 = """ + + + + +""" + +arrow_right = """ + + +""" + +select = """ + + +""" + +history = """ + + + + +""" + +clipboard = """ + + +""" + +radial_small = """ + + + + + + + + +""" + + +radial_big = """ + + + + + + + + +""" + +bottom_logo = """ + + + + + + + + +""" + +feather = """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +""" + +feather_unstyled = """""" + +cloud = """ + + + +""" + +database = """ + + + + + + + + +""" + +star = """ + + + +""" + +fork = """ + + + + + +""" + +eye_big = """ + + + +""" + +image_ai = """ + + + + +""" + +dice = """ + + + + + +""" + +contributors = """ + + + + + +""" + +badge_logo = """ + + + + +""" + +github_navbar = """ + + + + +""" + +discord_navbar = """ + + +""" + +send = """ + + +""" + +chat_bubble = """ + + +""" + +image_ai_small = """ + + + + +""" + +chart = """ + + +""" + +code_custom = """ + + + + + + + + + +""" + +message_form = """ + + +""" + +wave_pattern = """ + + + + + + + + + + + +""" + +backend_db = """ + + + + +""" + +backend_async = """ + + + +""" + +backend_compatible = """ + + + +""" + +backend_auth = """ + + + +""" + +copy_pip = """ + + + + + +""" + +forum = """ + + + +""" + +python = """ + + +""" + +db = """ + + +""" + +api = """ + +""" + +doc = """ + + +""" + +package = """ + + + +""" + +document_code = """ + + + + +""" + +infinity = """ + + + +""" + +analytics = """ + + +""" + +globe = """ + +""" + +chevron_right = """ + +""" + +ai_chat_02 = """ +""" + +dollar = """ + + + +""" + +webpage = """ + + + +""" + +arrow_top_right = """""" + +quote = """""" + +linkedin = """ + + + +""" + +image_03 = """ + + + +""" + +shield = """ + + +""" + +play_video = """ + + + +""" + +zap = """ + + +""" + +cancel_circle = """ +""" + +arrow_fill_down = """ + + +""" + +alert = """ + + +""" + +browser = """ + + + +""" + +checkmark = """ + + +""" +ai_04 = """ + +""" + +flow_connection = """ + +""" + +refresh_dot = """ + +""" + +layers_01 = """ + + + +""" + +zap_01 = """ + +""" + +python_01 = """ + +""" + +shield_key = """ + +""" + +chart_up = """ + +""" + +twitter_blog = """ + +""" + +linkedin_blog = """ + +""" + +link_blog = """ + + +""" + +reddit_blog = """ + + + + + +""" + +markdown = """""" + +twitter_footer = """ + +""" + +linkedin_footer = """ + +""" + +forum_footer = """ + + +""" + +moon_footer = """ + +""" + +sun_footer = """ + + +""" + +computer_footer = """ + + +""" +ICONS = { + # Socials + "github": github, + "discord": discord, + "twitter": twitter, + # Theme + "sun": sun, + "moon": moon, + "arrow_right": arrow_right, + "copy": copy, + "arrow_down": arrow_down, + "arrow_down_big": arrow_down_big, + "new_tab": new_tab, + "eye": eye, + "select": select, + "history": history, + "clipboard": clipboard, + "bottom_logo": bottom_logo, + "radial_small": radial_small, + "radial_big": radial_big, + "feather": feather, + "cloud": cloud, + "database": database, + "star": star, + "fork": fork, + "eye_big": eye_big, + "image_ai": image_ai, + "dice": dice, + "contributors": contributors, + "badge_logo": badge_logo, + "github_navbar": github_navbar, + "discord_navbar": discord_navbar, + "send": send, + "chat_bubble": chat_bubble, + "image_ai_small": image_ai_small, + "chart": chart, + "code_custom": code_custom, + "message_form": message_form, + "wave_pattern": wave_pattern, + "backend_db": backend_db, + "backend_async": backend_async, + "backend_compatible": backend_compatible, + "backend_auth": backend_auth, + "copy_pip": copy_pip, + "forum": forum, + "python": python, + "package": package, + "document_code": document_code, + "infinity": infinity, + "analytics": analytics, + "globe": globe, + "chevron_right": chevron_right, + "ai-chat-02": ai_chat_02, + "dollar": dollar, + "webpage": webpage, + "arrow_top_right": arrow_top_right, + "quote": quote, + "linkedin": linkedin, + "image-03": image_03, + "shield": shield, + "play_video": play_video, + "zap": zap, + "cancel-circle": cancel_circle, + "arrow-fill-down": arrow_fill_down, + "alert": alert, + "browser": browser, + "checkmark": checkmark, + "feather_unstyled": feather_unstyled, + "db": db, + "api": api, + "doc": doc, + "ai-04": ai_04, + "flow-connection": flow_connection, + "refresh-dot": refresh_dot, + "layers-01": layers_01, + "zap-01": zap_01, + "python-01": python_01, + "shield-key": shield_key, + "chart-up": chart_up, + "twitter_blog": twitter_blog, + "linkedin_blog": linkedin_blog, + "link_blog": link_blog, + "reddit_blog": reddit_blog, + "markdown": markdown, + "twitter_footer": twitter_footer, + "linkedin_footer": linkedin_footer, + "forum_footer": forum_footer, + "moon_footer": moon_footer, + "sun_footer": sun_footer, + "computer_footer": computer_footer, +} + + +def get_icon(icon: str, class_name: str = "", **props) -> rx.Component: + return rx.html( + ICONS[icon], + class_name=f"flex justify-center items-center {class_name}", + **props, + ) + + +def get_icon_var( + icon: str, class_name: rx.Var[str] | str = "", **props +) -> rx.Component: + """Get an icon component. + + Args: + icon (str): The name of the icon to retrieve. + class_name (str, optional): Additional CSS classes. Defaults to "". + **props: Additional properties to pass to the component. + + Returns: + rx.Component: The icon component. + + Raises: + KeyError: If the icon name is not found in ICONS. + + """ + return rx.html( + rx.Var.create(ICONS)[icon], + class_name=class_name, + **props, + ) diff --git a/shared/components/image_zoom.py b/shared/components/image_zoom.py new file mode 100644 index 0000000..e7948dd --- /dev/null +++ b/shared/components/image_zoom.py @@ -0,0 +1,23 @@ +"""Reflex custom component ImageZoom.""" + +import reflex as rx + + +class ImageZoom(rx.NoSSRComponent): + """ImageZoom component.""" + + # The React library to wrap. + library = "react-medium-image-zoom@5.4.2" + + # The React component tag. + tag = "Zoom" + + # If the tag is the default export from the module, you must set is_default = True. + is_default = True + + # To add custom code to your component + def _get_custom_code(self) -> str: + return "import 'react-medium-image-zoom/dist/styles.css'" + + +image_zoom = ImageZoom.create diff --git a/shared/components/marketing_button.py b/shared/components/marketing_button.py new file mode 100644 index 0000000..2a889f8 --- /dev/null +++ b/shared/components/marketing_button.py @@ -0,0 +1,37 @@ +"""Custom button component.""" + +from typing import Literal + +import reflex as rx +from reflex.vars.base import Var + +LiteralButtonVariant = Literal["primary", "destructive", "outline", "ghost"] +LiteralButtonSize = Literal[ + "xs", "sm", "md", "lg", "icon-xs", "icon-sm", "icon-md", "icon-lg" +] + + +class Button(rx.Component): + """A custom button component.""" + + library = "$/public/components/GradientButton" + + tag = "GradientButton" + + # Button variant. Defaults to "primary". + variant: Var[LiteralButtonVariant] + + # Button size. Defaults to "md". + size: Var[LiteralButtonSize] + + # Whether to use a native button element. Defaults to True. If False, the button will be rendered as a div element. + native_button: Var[bool] = rx.Var.create(True) + + def add_imports(self) -> rx.ImportDict: + """Add imports to the component.""" + return { + "clsx-for-tailwind": "cn", + } + + +button = Button.create diff --git a/shared/components/marquee.py b/shared/components/marquee.py new file mode 100644 index 0000000..bd6749f --- /dev/null +++ b/shared/components/marquee.py @@ -0,0 +1,31 @@ +from typing import Literal + +import reflex as rx + + +class Marquee(rx.NoSSRComponent): + """Marquee component.""" + + library = "react-fast-marquee@1.6.5" + tag = "Marquee" + is_default = True + + # Behavior props + auto_fill: rx.Var[bool] = rx.Var.create(True) + play: rx.Var[bool] = rx.Var.create(True) + pause_on_hover: rx.Var[bool] = rx.Var.create(True) + pause_on_click: rx.Var[bool] = rx.Var.create(False) + direction: rx.Var[Literal["left", "right", "up", "down"]] = rx.Var.create("left") + + # Animation props + speed: rx.Var[int] = rx.Var.create(35) + delay: rx.Var[int] = rx.Var.create(0) + loop: rx.Var[int] = rx.Var.create(0) + + # Gradient props + gradient: rx.Var[bool] = rx.Var.create(True) + gradient_color: rx.Var[str] = rx.Var.create("var(--c-slate-1)") + gradient_width: rx.Var[int | str] = rx.Var.create(250) + + +marquee = Marquee.create diff --git a/shared/components/patterns.py b/shared/components/patterns.py new file mode 100644 index 0000000..20e2534 --- /dev/null +++ b/shared/components/patterns.py @@ -0,0 +1,99 @@ +import reflex as rx + +from shared.components.icons import get_icon +from shared.constants import REFLEX_ASSETS_CDN +from shared.views.hosting_banner import HostingBannerState + + +def create_pattern( + pattern: str, + class_name: str, +) -> rx.Component: + return get_icon( + pattern, + class_name="z-[-1] absolute w-[1111.528px] h-[1094.945px] overflow-hidden pointer-events-none shrink-0" + + " " + + class_name, + ) + + +def default_patterns() -> list[rx.Component]: + return [ + # Left + create_pattern( + "radial_small", + class_name="top-0 mt-[-80px] mr-[725px] translate-y-0", + ), + create_pattern( + "radial_big", + class_name="top-0 mt-[90px] mr-[700px] translate-y-0 rotate-180 scale-x-[-1] scale-y-[-1]", + ), + # Right + create_pattern( + "radial_small", + class_name="top-0 mt-[-80px] ml-[725px] scale-x-[-1]", + ), + create_pattern( + "radial_big", + class_name="top-0 mt-[90px] ml-[700px] scale-x-[-1]", + ), + # Glowing + rx.box( + class_name="top-[715px] z-[-1] absolute bg-violet-3 opacity-[0.36] blur-[80px] rounded-[768px] w-[768px] h-[768px] overflow-hidden pointer-events-none shrink-0 left-1/2 transform -translate-x-1/2 -translate-y-1/2" + ), + ] + + +def index_patterns() -> list[rx.Component]: + return [ + rx.box( + get_icon("wave_pattern", class_name=""), + get_icon("wave_pattern", class_name="scale-x-[-1]"), + class_name="flex flex-row gap-[4.125rem] absolute top-0 left-1/2 transform -translate-x-1/2 lg:mt-[65px] mt-[0px] z-[-1] w-[64.15rem] overflow-hidden opacity-70 lg:opacity-100", + ), + # Glowing + rx.box( + class_name="bg-[radial-gradient(50%_50%_at_50%_50%,_var(--glow)_0%,_rgba(21,_22,_24,_0.00)_100%)] w-[56.0625rem] h-[35.3125rem] rounded-[56.0625rem] overflow-hidden pointer-events-none shrink-0 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-[-1] mt-[40rem] absolute top-0" + ), + # Glowing small + rx.box( + class_name="bg-[radial-gradient(50%_50%_at_50%_50%,_var(--glow)_0%,_rgba(21,_22,_24,_0.00)_100%)] w-[56.125rem] h-[11.625rem] rounded-[56.125rem] overflow-hidden pointer-events-none shrink-0 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-[-1] mt-[65.75rem] absolute top-0" + ), + ] + + +def landing_patterns() -> list[rx.Component]: + return [ + rx.box( + get_icon("wave_pattern", class_name=""), + get_icon("wave_pattern", class_name="scale-x-[-1]"), + class_name="flex flex-row gap-[4.125rem] absolute top-0 left-1/2 transform -translate-x-1/2 lg:mt-[65px] mt-[0px] z-[-1] w-[64.15rem] overflow-hidden opacity-70 lg:opacity-100", + ), + # Glowing small + rx.box( + class_name="bg-[radial-gradient(50%_50%_at_50%_50%,_var(--glow)_0%,_rgba(21,_22,_24,_0.00)_100%)] w-[56.125rem] h-[23.625rem] rounded-[56.125rem] overflow-hidden pointer-events-none shrink-0 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-[-1] mt-[27.75rem] absolute top-0 block lg:hidden" + ), + ] + + +def hosting_patterns() -> list[rx.Component]: + return [ + rx.image( + src=f"{REFLEX_ASSETS_CDN}hosting/light/hosting_patterns.svg", + alt="Reflex Hosting Patterns", + class_name=rx.cond( # type: ignore[arg-type] + HostingBannerState.is_banner_visible, + "dark:hidden lg:flex hidden absolute top-0 z-[-1] w-[1028px] h-[478px] pointer-events-none shrink-0 left-1/2 transform -translate-x-1/2 -translate-y-1/2 lg:mt-[24rem] mt-[3.5rem]", + "dark:hidden lg:flex hidden absolute top-0 z-[-1] w-[1028px] h-[478px] pointer-events-none shrink-0 left-1/2 transform -translate-x-1/2 -translate-y-1/2 lg:mt-[19rem] mt-[8.5rem]", + ), + ), + rx.image( + src=f"{REFLEX_ASSETS_CDN}hosting/dark/hosting_patterns.svg", + alt="Reflex Hosting Patterns", + class_name=rx.cond( # type: ignore[arg-type] + HostingBannerState.is_banner_visible, + "hidden dark:flex lg:dark:flex absolute top-0 z-[-1] w-[1028px] h-[478px] pointer-events-none shrink-0 left-1/2 transform -translate-x-1/2 -translate-y-1/2 lg:mt-[24rem] mt-[3.5rem]", + "hidden dark:flex lg:dark:flex absolute top-0 z-[-1] w-[1028px] h-[478px] pointer-events-none shrink-0 left-1/2 transform -translate-x-1/2 -translate-y-1/2 lg:mt-[19rem] mt-[8.5rem]", + ), + ), + ] diff --git a/shared/constants.py b/shared/constants.py new file mode 100644 index 0000000..c98fdbb --- /dev/null +++ b/shared/constants.py @@ -0,0 +1,36 @@ +import os + +CHANGELOG_URL = "https://github.com/reflex-dev/reflex/releases" +CONTRIBUTING_URL = "https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md" +DISCUSSIONS_URL = "https://github.com/orgs/reflex-dev/discussions" +GITHUB_STARS = 28000 +GITHUB_URL = "https://github.com/reflex-dev/reflex" +JOBS_BOARD_URL = "https://www.ycombinator.com/companies/reflex/jobs" +REFLEX_ASSETS_CDN = "https://web.reflex-assets.dev/" +SCREENSHOT_BUCKET = "https://pub-c14a5dcf674640a6b73fded32bad72ca.r2.dev/" +INTEGRATIONS_IMAGES_URL = "https://raw.githubusercontent.com/reflex-dev/integrations-docs/refs/heads/main/images/logos/" +REFLEX_BUILD_URL = os.getenv("REFLEX_BUILD_URL", "https://build.reflex.dev/") +PIP_URL = "https://pypi.org/project/reflex" +GITHUB_URL = "https://github.com/reflex-dev/reflex" +LINKEDIN_URL = "https://www.linkedin.com/company/reflex-dev" +OLD_GITHUB_URL = "https://github.com/pynecone-io/pynecone" +GITHUB_DISCUSSIONS_URL = "https://github.com/orgs/reflex-dev/discussions" +FORUM_URL = "https://forum.reflex.dev" +TWITTER_URL = "https://twitter.com/getreflex" +DISCORD_URL = "https://discord.gg/T5WSbC2YtQ" +ROADMAP_URL = "https://github.com/reflex-dev/reflex/issues/2727" + +REFLEX_URL = "https://reflex.dev/" +REFLEX_DOMAIN_URL = "https://reflex.dev/" +REFLEX_DOMAIN = "reflex.dev" +TWITTER_CREATOR = "@getreflex" + + +API_BASE_URL_LOOPS: str = "https://app.loops.so/api/v1" +REFLEX_DEV_WEB_NEWSLETTER_FORM_WEBHOOK_URL: str = "https://hkdk.events/t0qopjbznnp2fr" +REFLEX_DEV_WEB_GENERAL_FORM_FEEDBACK_WEBHOOK_URL: str = os.environ.get( + "REFLEX_DEV_WEB_GENERAL_FORM_FEEDBACK_WEBHOOK_URL", "" +) +RECENT_BLOGS_API_URL: str = os.environ.get( + "RECENT_BLOGS_API_URL", "https://reflex.dev/api/v1/recent-blogs" +) diff --git a/shared/gallery/__init__.py b/shared/gallery/__init__.py new file mode 100644 index 0000000..ef105eb --- /dev/null +++ b/shared/gallery/__init__.py @@ -0,0 +1 @@ +from .gallery import gallery as gallery diff --git a/shared/gallery/apps.py b/shared/gallery/apps.py new file mode 100644 index 0000000..9c2e33d --- /dev/null +++ b/shared/gallery/apps.py @@ -0,0 +1,286 @@ +import copy +import re + +import flexdown +import reflex as rx + +import reflex_ui as ui +from reflex_ui.blocks.demo_form import demo_form_dialog +from shared.components.blocks.flexdown import xd +from shared.components.code_card import gallery_app_card +from shared.components.icons import get_icon +from shared.constants import REFLEX_ASSETS_CDN, SCREENSHOT_BUCKET +from shared.gallery.gallery import integrations_stack +from shared.templates.gallery_app_page import gallery_app_page + +GALLERY_APP_SOURCES = [ + ("templates/", "docs/getting-started/open-source-templates/"), + ("reflex_build_templates/", "templates/"), +] + + +def integration_image(integration: str): + integration_logo = integration.replace(" ", "_").lower() + return ui.tooltip( + trigger=ui.avatar.root( + ui.avatar.image( + src=rx.color_mode_cond( + f"{REFLEX_ASSETS_CDN}integrations/light/{integration_logo}.svg", + f"{REFLEX_ASSETS_CDN}integrations/dark/{integration_logo}.svg", + ), + unstyled=True, + class_name="size-full", + ), + ui.avatar.fallback( + unstyled=True, + ), + unstyled=True, + class_name="size-5 flex items-center justify-center", + ), + content=integration, + ) + + +def load_all_gallery_apps(): + """Load markdown files from all supported paths and associate them with their base folder.""" + gallery_apps = {} + for folder, _ in GALLERY_APP_SOURCES: + paths = flexdown.utils.get_flexdown_files(folder) + for path in sorted(paths, reverse=True): + document = flexdown.Document.from_file(path) # This has metadata + document.metadata["title"] = document.metadata.get("title", "Untitled") + clean_path = str(path).replace(".md", "/") + gallery_apps[(clean_path, folder)] = document + return gallery_apps + + +gallery_apps_data = load_all_gallery_apps() +gallery_apps_data_copy = {path: doc for (path, _), doc in gallery_apps_data.items()} +gallery_apps_data_open_source = { + (path, folder): doc + for (path, folder), doc in load_all_gallery_apps().items() + if folder == "templates/" +} + + +def more_posts(current_post: dict) -> rx.Component: + posts = [] + app_copy = copy.deepcopy(gallery_apps_data) + app_items = list(app_copy.items()) + current_index = next( + ( + i + for i, (path, document) in enumerate(app_items) + if document.metadata.get("title") == current_post.get("title") + ), + None, + ) + + if current_index is None: + selected_posts = app_items[-3:] + else: + other_posts = app_items[:current_index] + app_items[current_index + 1 :] + if len(other_posts) <= 3: + selected_posts = other_posts + elif current_index == 0: + selected_posts = other_posts[:3] + elif current_index >= len(app_items) - 1: + selected_posts = other_posts[-3:] + else: + if current_index < len(app_items) - 2: + selected_posts = other_posts[current_index - 1 : current_index + 2] + else: + selected_posts = other_posts[current_index - 2 : current_index + 1] + + for _path, document in selected_posts: + if not _path[0].startswith("reflex_build_templates/"): + posts.append(gallery_app_card(app=document.metadata)) + + return rx.el.section( + rx.box( + rx.el.h2("More Templates", class_name="font-large text-slate-12"), + rx.link( + rx.box( + rx.text( + "View All", class_name="font-small text-slate-9 text-nowrap" + ), + get_icon(icon="new_tab", class_name=""), + class_name="flex items-center gap-1.5 border-slate-5 bg-slate-1 hover:bg-slate-3 shadow-small px-1.5 py-0.5 border rounded-md w-auto max-w-full text-slate-9 transition-bg cursor-pointer overflow-hidden border-solid", + ), + underline="none", + href="/docs/getting-started/open-source-templates/", + ), + class_name="flex flex-row items-center justify-between gap-4", + ), + rx.box( + *posts, + class_name="gap-6 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 [&>*]:min-w-[300px] w-full mb-4 blog-grid", + ), + class_name="flex flex-col gap-10 mt-20 p-8 border-t border-slate-3", + ) + + +def page(document: flexdown.Document, is_reflex_template: bool) -> rx.Component: + """Render a detailed app page based on source type.""" + meta = document.metadata + + image_component = ( + rx.image( + src=meta["image"], + alt=f"Image for Reflex App: {meta['title']}", + loading="lazy", + class_name="w-full object-cover max-w-full aspect-[1500/938] border-y border-slate-3 border-solid", + ) + if not is_reflex_template + else rx.el.div( + rx.box( + rx.el.h1( + meta["title"].replace("_", " ").title(), + class_name="font-x-large text-slate-12 text-left", + ), + class_name="w-full self-start pl-4", + ), + rx.el.iframe( + src=meta["video"], + class_name="w-full h-full xl:rounded-md shadow-small", + id="iFrame", + title="Reflex Build", + frameborder="0", + ), + class_name="w-full h-[80vh] text-center flex flex-col gap-y-4 items-center text-slate-10", + ) + ) + + back_route_origin = ( + "/docs/getting-started/open-source-templates/" + if not is_reflex_template + else "/templates/" + ) + + return rx.el.section( + rx.el.article( + image_component, + rx.box( + rx.el.header( + rx.link( + rx.box( + get_icon("arrow_right", class_name="rotate-180"), + "Back to Templates", + class_name="box-border flex justify-center items-center gap-2 bg-slate-1 py-0.5 font-small text-slate-9 transition-color cursor-pointer hover:text-slate-11 mb-6", + ), + underline="none", + class_name="flex w-fit", + href=back_route_origin, + ), + ( + rx.el.h1(meta["title"], class_name="font-x-large text-slate-12") + if not is_reflex_template + else rx.fragment() + ), + rx.el.h2(meta["description"], class_name="font-md text-slate-11"), + ( + rx.el.div( + rx.el.span( + "Integrations: ", class_name="text-slate-9 font-base" + ), + rx.el.div( + integrations_stack(meta.get("integrations", [])), + class_name="flex flex-row gap-3.5 items-center", + ), + class_name="flex flex-row items-center gap-2 mt-2", + ) + if meta.get("integrations") + else rx.fragment() + ), + class_name="flex flex-col gap-3 p-8", + ), + rx.box( + *( + [ + rx.box( + demo_form_dialog( + trigger=ui.button( + ui.icon("LinkSquare01Icon"), + "Book a Demo", + class_name="flex-row-reverse gap-2 !w-full", + ), + ), + class_name="flex justify-center items-center h-full !w-full [&_button]:!w-full", + ) + ] + ), + ( + rx.link( + ui.button( + "View Code", variant="secondary", class_name="!w-full" + ), + is_external=True, + href=meta.get("source", "#"), + ) + if not is_reflex_template + else rx.fragment() + ), + ( + rx.cond( + "Reflex" in meta["author"], + rx.box( + rx.text( + "Created by", class_name="text-slate-9 font-base" + ), + get_icon(icon="badge_logo"), + rx.text( + meta["author"], class_name="text-slate-9 font-base" + ), + class_name="flex flex-row items-center gap-1 self-end", + ), + rx.text( + f"by {meta['author']}", + class_name="text-slate-9 font-base", + ), + ) + if not is_reflex_template + else rx.fragment() + ), + class_name="p-8 flex flex-col gap-4", + ), + class_name="grid grid-cols-1 lg:grid-cols-2 divide-y lg:divide-y-0 lg:divide-x divide-slate-3 border-b border-slate-3", + ), + rx.box( + xd.render(document, "blog.md"), + class_name="flex flex-col gap-4 w-full p-8", + ), + more_posts(meta) if not is_reflex_template else rx.fragment(), + class_name="flex flex-col max-w-full", + ), + ) + + +gallery_apps_routes = [] +for (_path, source_folder), document in gallery_apps_data.items(): + is_reflex_template = source_folder.startswith("reflex_build_templates") + base_url = ( + "templates/" + if is_reflex_template + else "docs/getting-started/open-source-templates/" + ) + slug = re.sub(r"[\s_]+", "-", document.metadata["title"]).lower() + route = f"/{base_url}{slug}" + + document.metadata["image"] = ( + f"{REFLEX_ASSETS_CDN}reflex_build_template_images/{document.metadata['image']}" + if is_reflex_template and not document.metadata.get("ai_template", False) + else f"{REFLEX_ASSETS_CDN}templates/{document.metadata['image']}" + if not document.metadata.get("ai_template", False) + else f"{SCREENSHOT_BUCKET}{document.metadata['image']}" + ) + + comp = gallery_app_page( + path=route, + title=document.metadata["title"], + description=document.metadata.get("description", ""), + image=document.metadata["image"], + demo=document.metadata.get("demo"), + meta=document.metadata.get("meta", []), + )(lambda doc=document, is_rt=is_reflex_template: page(doc, is_rt)) + + gallery_apps_routes.append(comp) diff --git a/shared/gallery/common.py b/shared/gallery/common.py new file mode 100644 index 0000000..71a0cf7 --- /dev/null +++ b/shared/gallery/common.py @@ -0,0 +1,242 @@ +import re + +import flexdown +import reflex as rx + +import reflex_ui as ui +from shared.constants import INTEGRATIONS_IMAGES_URL, REFLEX_ASSETS_CDN +from shared.gallery.r_svg_loader import r_svg_loader + +REFLEX_BUILD_TEMPLATES_PATH = "reflex_build_templates/" +REFLEX_BUILD_TEMPLATES_IMAGES = "reflex_build_template_images/" + + +def get_templatey_apps(paths: list): + """Method to parse each markdown file and return the data from the file.""" + gallery_apps = {} + for path in sorted(paths, reverse=True): + document = flexdown.Document.from_file(path) # This has metadata + key = str(path).replace(".md", "/") + gallery_apps[key] = document + return gallery_apps + + +paths = flexdown.utils.get_flexdown_files(REFLEX_BUILD_TEMPLATES_PATH) +template_apps_data = get_templatey_apps(paths) + + +def app_dialog_with_trigger( + app_url: str, + app_name: str, + app_author: str, + app_thread: str, + app_inner_page: str, + trigger_content: rx.Component, + app_video_url: str, +): + return rx.dialog.root( + rx.dialog.trigger(trigger_content, class_name="w-full h-full"), + rx.dialog.content( + rx.el.div( + rx.el.div( + r_svg_loader(), + class_name="absolute inset-0 flex items-center justify-center", + ), + rx.el.div( + rx.el.div( + rx.el.p( + app_name, class_name="text-md !text-slate-11 font-bold" + ), + rx.el.p(app_author, class_name="text-sm !text-slate-9"), + class_name="flex flex-row gap-x-2 items-center", + ), + rx.link( + ui.button( + "Learn More", + variant="secondary", + size="md", + class_name="!text-secondary-12", + ), + href=app_inner_page, + class_name="no-underline outline-none", + ), + class_name="flex flex-row items-center justify-between", + ), + rx.el.iframe( + src=app_video_url, + class_name="w-full h-full xl:rounded-md shadow-small z-10", + id="iFrame", + title="Reflex Build", + frameborder="0", + ), + class_name="flex flex-col w-full h-full gap-y-3 relative", + ), + class_name="w-full !max-w-[90em] xl:max-w-[110em] 2xl:max-w-[120em] h-[80vh] font-sans", + ), + ) + + +def integration_image(integration: str, class_name: str = ""): + integration_logo = integration.replace(" ", "_").lower() + return ui.avatar.root( + ui.avatar.image( + src=rx.color_mode_cond( + f"{INTEGRATIONS_IMAGES_URL}light/{integration_logo}.svg", + f"{INTEGRATIONS_IMAGES_URL}dark/{integration_logo}.svg", + ), + unstyled=True, + class_name="size-full", + ), + ui.avatar.fallback( + unstyled=True, + ), + unstyled=True, + class_name=ui.cn("size-4 flex items-center justify-center", class_name), + ) + + +def integrations_stack(integrations: list[str]) -> rx.Component: + return rx.el.div( + rx.foreach( + integrations, + lambda integration: rx.el.div( + ui.tooltip( + trigger=rx.el.div( + integration_image(integration, class_name="size-4"), + class_name="size-8 shrink-0 flex justify-center items-center rounded-full shadow-small border border-secondary-a5 bg-white-1 dark:bg-secondary-1 cursor-default", + ), + side="bottom", + content=integration, + ), + ), + ), + class_name="flex flex-row -space-x-2 flex-wrap gap-y-2", + ) + + +def extended_gallery_grid_item( + app_url: str, + app_name: str, + app_author: str, + app_thread: str, + app_image: str, + app_inner_page: str, + app_video_url: str, + app_integrations: list[str], +): + return app_dialog_with_trigger( + app_url=app_url, + app_author=app_author, + app_name=app_name, + app_thread=app_thread, + app_inner_page=app_inner_page, + app_video_url=app_video_url, + trigger_content=rx.el.div( + rx.el.div( + rx.el.div( + rx.image( + src=app_image, + class_name="group-hover:scale-105 duration-200 ease-out object-center object-cover absolute inset-0 size-full blur-in transition-all z-10", + ), + rx.el.div( + rx.el.div( + rx.link( + ui.button( + "Learn More", + variant="secondary", + size="md", + class_name="w-full !text-secondary-12", + on_click=rx.stop_propagation, + ), + href=app_inner_page, + class_name="no-underline flex-1", + on_click=rx.stop_propagation, + ), + ui.button( + "Preview", + variant="primary", + size="md", + class_name="flex-1 shadow-none border-none", + ), + class_name="flex flex-row gap-x-2 w-full items-stretch px-4 pb-4", + ), + class_name="absolute inset-0 flex items-end justify-center opacity-0 translate-y-2 group-hover:opacity-100 group-hover:translate-y-0 transition-all duration-300 ease-out pointer-events-none group-hover:pointer-events-auto z-[99]", + ), + class_name="overflow-hidden relative size-full aspect-video ease-out transition-all outline-none ", + ), + rx.el.div( + rx.el.span( + app_name, + class_name="text-sm font-semibold text-slate-12 dark:text-m-slate-3 truncate min-w-0 max-w-[90%]", + ), + rx.el.div( + rx.el.span( + "App Integrations: ", + class_name="text-slate-9 text-sm font-medium", + ), + rx.el.div( + integrations_stack(app_integrations), + class_name="flex flex-row gap-3.5 items-center flex-wrap", + ), + class_name="flex flex-row items-center gap-2 mt-2", + ), + class_name=( + "flex flex-col w-full px-4 py-3 border-t border-m-slate-4 dark:border-m-slate-12 gap-2 relative pb-4", + ), + ), + class_name="flex flex-col w-full", + ), + key=app_name, + class_name="group cursor-pointer rounded-2xl shadow-small border border-slate-4 dark:border-m-slate-12 bg-white-1 dark:bg-m-slate-14 flex flex-col w-full relative overflow-hidden", + ), + ) + + +def create_grid_with_items(): + items = [] + for document in template_apps_data.values(): + meta = document.metadata + app_url = meta.get("demo", "#") + app_name = meta.get("title", "Untitled").replace("_", " ").title() + app_author = meta.get("author", "Team Reflex") + app_thread = f"/gen/{app_name.lower().replace(' ', '-')}/" + app_image = meta.get("image", "") + slug = re.sub(r"[\s_]+", "-", meta.get("title", "")).lower() + app_inner_page = f"/templates/{slug}" + app_video_url = meta.get("video", "#") + app_integrations = meta.get("integrations", []) + + items.append( + extended_gallery_grid_item( + app_url=app_url, + app_name=app_name, + app_author=app_author, + app_thread=app_thread, + app_image=f"{REFLEX_ASSETS_CDN}{REFLEX_BUILD_TEMPLATES_IMAGES}{app_image}", + app_inner_page=app_inner_page, + app_video_url=app_video_url, + app_integrations=app_integrations, + ) + ) + + return rx.el.div( + *items, + class_name="grid grid-cols-1 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 md:px-8 lg:px-8", + ) + + +def create_header(): + return rx.box( + rx.box( + rx.el.h1( + "Reflex Build Templates", + class_name="text-slate-12 text-4xl font-bold mb-6", + ), + rx.el.p( + "Production-ready app templates built with Reflex — explore dashboards, tools, and AI-powered apps.", + class_name="text-slate-11 text-lg leading-relaxed mb-12 max-w-lg font-medium", + ), + class_name="mb-8 lg:mb-0 text-center", + ), + class_name="flex flex-col justify-center items-center gap-6 w-full text-center", + ) diff --git a/shared/gallery/gallery.py b/shared/gallery/gallery.py new file mode 100644 index 0000000..57e622b --- /dev/null +++ b/shared/gallery/gallery.py @@ -0,0 +1,260 @@ +import re + +import flexdown +import reflex as rx + +import reflex_ui as ui +from shared.constants import INTEGRATIONS_IMAGES_URL, REFLEX_ASSETS_CDN +from shared.gallery.r_svg_loader import r_svg_loader +from shared.templates.webpage import webpage + +REFLEX_BUILD_TEMPLATES_PATH = "reflex_build_templates/" +REFLEX_BUILD_TEMPLATES_IMAGES = "reflex_build_template_images/" + + +def get_templatey_apps(paths: list): + """Method to parse each markdown file and return the data from the file.""" + gallery_apps = {} + for path in sorted(paths, reverse=True): + document = flexdown.Document.from_file(path) # This has metadata + key = str(path).replace(".md", "/") + gallery_apps[key] = document + return gallery_apps + + +paths = flexdown.utils.get_flexdown_files(REFLEX_BUILD_TEMPLATES_PATH) +template_apps_data = get_templatey_apps(paths) + + +def app_dialog_with_trigger( + app_url: str, + app_name: str, + app_author: str, + app_thread: str, + app_inner_page: str, + trigger_content: rx.Component, + app_video_url: str, +): + return rx.dialog.root( + rx.dialog.trigger(trigger_content, class_name="w-full h-full"), + rx.dialog.content( + rx.el.div( + rx.el.div( + r_svg_loader(), + class_name="absolute inset-0 flex items-center justify-center", + ), + rx.el.div( + rx.el.div( + rx.el.p( + app_name, class_name="text-md !text-slate-11 font-bold" + ), + rx.el.p(app_author, class_name="text-sm !text-slate-9"), + class_name="flex flex-row gap-x-2 items-center", + ), + rx.link( + ui.button( + "Learn More", + variant="secondary", + size="md", + class_name="!text-secondary-12", + ), + href=app_inner_page, + class_name="no-underline outline-none", + ), + class_name="flex flex-row items-center justify-between", + ), + rx.el.iframe( + src=app_video_url, + class_name="w-full h-full xl:rounded-md shadow-small z-10", + id="iFrame", + title="Reflex Build", + frameborder="0", + ), + class_name="flex flex-col w-full h-full gap-y-3 relative", + ), + class_name="w-full !max-w-[90em] xl:max-w-[110em] 2xl:max-w-[120em] h-[80vh] font-sans", + ), + ) + + +def integration_image(integration: str, class_name: str = ""): + integration_logo = integration.replace(" ", "_").lower() + return ui.avatar.root( + ui.avatar.image( + src=rx.color_mode_cond( + f"{INTEGRATIONS_IMAGES_URL}light/{integration_logo}.svg", + f"{INTEGRATIONS_IMAGES_URL}dark/{integration_logo}.svg", + ), + unstyled=True, + class_name="size-full", + ), + ui.avatar.fallback( + unstyled=True, + ), + unstyled=True, + class_name=ui.cn("size-4 flex items-center justify-center", class_name), + ) + + +def integrations_stack(integrations: list[str]) -> rx.Component: + return rx.el.div( + rx.foreach( + integrations, + lambda integration: rx.el.div( + ui.tooltip( + trigger=rx.el.div( + integration_image(integration, class_name="size-4"), + class_name="size-8 shrink-0 flex justify-center items-center rounded-full shadow-small border border-secondary-a5 bg-white-1 dark:bg-secondary-1 cursor-default", + ), + side="bottom", + content=integration, + ), + ), + ), + class_name="flex flex-row -space-x-2 flex-wrap gap-y-2", + ) + + +def extended_gallery_grid_item( + app_url: str, + app_name: str, + app_author: str, + app_thread: str, + app_image: str, + app_inner_page: str, + app_video_url: str, + app_integrations: list[str], +): + return app_dialog_with_trigger( + app_url=app_url, + app_author=app_author, + app_name=app_name, + app_thread=app_thread, + app_inner_page=app_inner_page, + app_video_url=app_video_url, + trigger_content=rx.el.div( + rx.el.div( + rx.el.div( + rx.image( + src=app_image, + class_name="group-hover:scale-105 duration-200 ease-out object-center object-cover absolute inset-0 size-full blur-in transition-all z-10", + ), + rx.el.div( + rx.el.div( + rx.link( + ui.button( + "Learn More", + variant="secondary", + size="md", + class_name="w-full !text-secondary-12", + on_click=rx.stop_propagation, + ), + href=app_inner_page, + class_name="no-underline flex-1", + on_click=rx.stop_propagation, + ), + ui.button( + "Preview", + variant="primary", + size="md", + class_name="flex-1 shadow-none border-none", + ), + class_name="flex flex-row gap-x-2 w-full items-stretch px-4 pb-4", + ), + class_name="absolute inset-0 flex items-end justify-center opacity-0 translate-y-2 group-hover:opacity-100 group-hover:translate-y-0 transition-all duration-300 ease-out pointer-events-none group-hover:pointer-events-auto z-[99]", + ), + class_name="overflow-hidden relative size-full aspect-video ease-out transition-all outline-none ", + ), + rx.el.div( + rx.el.span( + app_name, + class_name="text-sm font-semibold text-slate-12 dark:text-m-slate-3 truncate min-w-0 max-w-[90%]", + ), + rx.el.div( + rx.el.span( + "App Integrations: ", + class_name="text-slate-9 text-sm font-medium", + ), + rx.el.div( + integrations_stack(app_integrations), + class_name="flex flex-row gap-3.5 items-center flex-wrap", + ), + class_name="flex flex-row items-center gap-2 mt-2", + ), + class_name=( + "flex flex-col w-full px-4 py-3 border-t border-m-slate-4 dark:border-m-slate-12 gap-2 relative pb-4", + ), + ), + class_name="flex flex-col w-full", + ), + key=app_name, + class_name="group cursor-pointer rounded-2xl shadow-small border border-slate-4 dark:border-m-slate-12 bg-white-1 dark:bg-m-slate-14 flex flex-col w-full relative overflow-hidden", + ), + ) + + +def create_grid_with_items(): + items = [] + for document in template_apps_data.values(): + meta = document.metadata + app_url = meta.get("demo", "#") + app_name = meta.get("title", "Untitled").replace("_", " ").title() + app_author = meta.get("author", "Team Reflex") + app_thread = f"/gen/{app_name.lower().replace(' ', '-')}/" + app_image = meta.get("image", "") + slug = re.sub(r"[\s_]+", "-", meta.get("title", "")).lower() + app_inner_page = f"/templates/{slug}" + app_video_url = meta.get("video", "#") + app_integrations = meta.get("integrations", []) + + items.append( + extended_gallery_grid_item( + app_url=app_url, + app_name=app_name, + app_author=app_author, + app_thread=app_thread, + app_image=f"{REFLEX_ASSETS_CDN}{REFLEX_BUILD_TEMPLATES_IMAGES}{app_image}", + app_inner_page=app_inner_page, + app_video_url=app_video_url, + app_integrations=app_integrations, + ) + ) + + return rx.el.div( + *items, + class_name="grid grid-cols-1 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 md:px-8 lg:px-8", + ) + + +def create_header(): + return rx.box( + rx.box( + rx.el.h1( + "Reflex Build Templates", + class_name="text-slate-12 text-4xl font-bold mb-6", + ), + rx.el.p( + "Production-ready app templates built with Reflex — explore dashboards, tools, and AI-powered apps.", + class_name="text-slate-11 text-lg leading-relaxed mb-12 max-w-lg font-medium", + ), + class_name="mb-8 lg:mb-0 text-center", + ), + class_name="flex flex-col justify-center items-center gap-6 w-full text-center", + ) + + +@webpage( + path="/templates", + title="Reflex App Templates - Python Dashboards & Tools", + description="Reflex app templates: dashboards, chatbots, data tools, and AI apps. Start from a template and customize in Python.", +) +def gallery() -> rx.Component: + return rx.el.section( + rx.box( + create_header(), + create_grid_with_items(), + class_name="w-full !max-w-[94.5rem] mx-auto", + ), + id="gallery", + class_name="w-full px-4 pt-24 lg:pt-52 mt-4 mb-20", + ) diff --git a/shared/gallery/r_svg_loader.py b/shared/gallery/r_svg_loader.py new file mode 100644 index 0000000..6049ee2 --- /dev/null +++ b/shared/gallery/r_svg_loader.py @@ -0,0 +1,123 @@ +import reflex as rx + +loading_style = { + "rect": { + "opacity": "0", + "animation": "fadeInStayOut 6s linear infinite", + }, + "@keyframes fadeInStayOut": { + "0%": {"opacity": "0"}, + "5%": {"opacity": "1"}, + "50%": {"opacity": "1"}, + "55%": {"opacity": "0"}, + "100%": {"opacity": "0"}, + }, +} + +for i in range(1, 14): + loading_style[f"rect:nth-child({i})"] = {"animation_delay": f"{(i - 1) * 0.2}s"} + + +def svg_loading(): + return rx.box( + rx.html( + """ + + + + + + + + + + + + + + """ + ), + style=loading_style, + position="absolute", + ) + + +spinner_style = { + "g rect": { + "transform-origin": "0 0", + }, + "g rect:nth-of-type(1)": { + "animation": "growShrinkWidth 3s linear infinite", + "width": "0", + }, + "g rect:nth-of-type(2)": { + "animation": "growShrinkHeight 3s linear infinite 0.35s", + "height": "0", + }, + "g rect:nth-of-type(3)": { + "animation": "growShrinkWidthReverse 3s linear infinite 0.7s", + "width": "0", + }, + "g rect:nth-of-type(4)": { + "animation": "growShrinkHeightReverse 3s linear infinite 1.05s", + "height": "0", + }, + "@keyframes growShrinkWidth": { + "0%": {"width": "0", "transform": "translateX(0)"}, + "12.5%": {"width": "40px", "transform": "translateX(0)"}, + "37.5%": {"width": "40px", "transform": "translateX(0)"}, + "50%": {"width": "40px", "transform": "translateX(0)"}, + "62.5%": {"width": "0", "transform": "translateX(40px)"}, + "100%": {"width": "0", "transform": "translateX(40px)"}, + }, + "@keyframes growShrinkHeight": { + "0%": {"height": "0", "transform": "translateY(0)"}, + "12.5%": {"height": "40px", "transform": "translateY(0)"}, + "37.5%": {"height": "40px", "transform": "translateY(0)"}, + "50%": {"height": "40px", "transform": "translateY(0)"}, + "62.5%": {"height": "0", "transform": "translateY(40px)"}, + "100%": {"height": "0", "transform": "translateY(40px)"}, + }, + "@keyframes growShrinkWidthReverse": { + "0%": {"width": "0", "transform": "translateX(41px)"}, + "12.5%": {"width": "41px", "transform": "translateX(0)"}, + "37.5%": {"width": "41px", "transform": "translateX(0)"}, + "50%": {"width": "41px", "transform": "translateX(0)"}, + "62.5%": {"width": "0", "transform": "translateX(0)"}, + "100%": {"width": "0", "transform": "translateX(0)"}, + }, + "@keyframes growShrinkHeightReverse": { + "0%": {"height": "0", "transform": "translateY(40px)"}, + "12.5%": {"height": "40px", "transform": "translateY(0)"}, + "37.5%": {"height": "40px", "transform": "translateY(0)"}, + "50%": {"height": "40px", "transform": "translateY(0)"}, + "62.5%": {"height": "0", "transform": "translateY(0)"}, + "100%": {"height": "0", "transform": "translateY(0)"}, + }, +} + + +def spinner_svg(mask_name: str): + return rx.box( + rx.html( + f""" + + + + + + + + + + """ + ), + style=spinner_style, + ) + + +def r_svg_loader(): + return rx.fragment( + spinner_svg(mask_name="mask"), + svg_loading(), + ) diff --git a/shared/gallery/sidebar.py b/shared/gallery/sidebar.py new file mode 100644 index 0000000..69a9360 --- /dev/null +++ b/shared/gallery/sidebar.py @@ -0,0 +1,194 @@ +import reflex as rx + +import reflex_ui as ui +from shared.gallery.apps import gallery_apps_data + +TAGS = { + "Category": [ + "AI/ML", + "Dashboard", + "Chat", + "Data Visualization", + "Image Generation", + "API Tools", + "Sports", + "DevOps", + ], +} + +ITEMS_PER_PAGE = 12 +TEMPLATES_FOLDER = "templates/" + + +TEMPLATE_SUMMARIES = [ + { + "title": (m := doc.metadata or {}).get("title", ""), + "description": m.get("description", ""), + "tags": m.get("tags", []), + } + for (_, folder), doc in gallery_apps_data.items() + if folder == TEMPLATES_FOLDER +] + + +class TemplatesState(rx.State): + query: rx.Field[str] = rx.field(default="") + checked_tags: rx.Field[set[str]] = rx.field(default_factory=set) + page: rx.Field[int] = rx.field(default=1) + + @rx.event + def clear_filters(self): + self.checked_tags = set() + self.page = 1 + + @rx.var + def all_filtered_templates(self) -> list[str]: + query = self.query.strip().lower() + return [ + t["title"] + for t in TEMPLATE_SUMMARIES + if ( + not query + or query in t["title"].lower() + or query in t["description"].lower() + ) + and (not self.checked_tags or set(t["tags"]) & self.checked_tags) + ] + + @rx.var + def total_pages(self) -> int: + return max(1, -(-len(self.all_filtered_templates) // ITEMS_PER_PAGE)) + + @rx.var + def filtered_templates(self) -> list[str]: + start = (self.page - 1) * ITEMS_PER_PAGE + return self.all_filtered_templates[start : start + ITEMS_PER_PAGE] + + @rx.event + def set_query(self, value: str): + self.query = value + self.page = 1 + + @rx.event + def toggle_template(self, value: str): + if value in self.checked_tags: + self.checked_tags.remove(value) + else: + self.checked_tags.add(value) + self.page = 1 + + @rx.event + def prev_page(self): + if self.page > 1: + self.page -= 1 + + @rx.event + def next_page(self): + if self.page < self.total_pages: + self.page += 1 + + +def pagination() -> rx.Component: + return rx.box( + rx.box( + ui.button( + ui.icon("ArrowLeft01Icon"), + disabled=TemplatesState.page == 1, + variant="secondary", + size="icon-sm", + on_click=TemplatesState.prev_page, + ), + ui.button( + ui.icon("ArrowRight01Icon"), + disabled=TemplatesState.page == TemplatesState.total_pages, + variant="secondary", + size="icon-sm", + on_click=TemplatesState.next_page, + ), + class_name="flex flex-row items-center gap-2", + ), + rx.text( + f"{TemplatesState.page} of {TemplatesState.total_pages}", + class_name="text-sm text-slate-12 font-medium", + ), + class_name="flex flex-row items-center gap-6 mt-10", + ) + + +def checkbox_item(text: str, value: str): + return rx.box( + rx.checkbox( + checked=TemplatesState.checked_tags.contains(value), + color_scheme="violet", + key=value, + class_name="cursor-pointer", + ), + rx.text( + text, + class_name="text-sm font-medium text-slate-12 font-sans cursor-pointer", + ), + on_click=TemplatesState.toggle_template(value), + class_name="flex items-center gap-2 px-3 py-2 rounded-md bg-slate-3 hover:bg-slate-4 transition-colors cursor-pointer", + ) + + +def filter_section(title: str, content: list[str]): + return rx.accordion.item( + rx.accordion.trigger( + rx.el.h3( + title, class_name="font-semibold text-base text-slate-12 text-start" + ), + rx.icon( + tag="chevron-down", + size=19, + class_name="!text-slate-11 group-data-[state=open]:rotate-180 transition-transform", + ), + class_name="hover:!bg-transparent !p-[0.5rem_0rem] !justify-between gap-4 group !mb-2", + ), + rx.accordion.content( + rx.box( + *[checkbox_item(item, item) for item in content], + class_name="flex flex-row gap-2 flex-wrap", + ), + class_name="before:!h-0 after:!h-0 radix-state-open:animate-accordion-down radix-state-closed:animate-accordion-up transition-all !px-0", + ), + value=title, + class_name="!p-0 w-full !bg-transparent !rounded-none !shadow-none", + ) + + +def sidebar() -> rx.Component: + return rx.box( + rx.box( + rx.box( + rx.el.h4( + "Filter Templates", + class_name="text-base font-semibold text-slate-12", + ), + rx.cond( + TemplatesState.checked_tags, + rx.el.p( + f"Clear filters ({TemplatesState.checked_tags.length()})", + on_click=TemplatesState.clear_filters, + class_name="text-sm text-slate-9 underline hover:text-slate-11 transition-colors cursor-pointer", + ), + ), + class_name="flex flex-row items-center gap-2 justify-between", + ), + ui.input( + icon="Search01Icon", + placeholder="Search...", + class_name="w-full", + on_change=TemplatesState.set_query.debounce(300), + clear_button_event=TemplatesState.set_query(""), + ), + class_name="flex flex-col gap-2", + ), + rx.accordion.root( + *[filter_section(title, content) for title, content in TAGS.items()], + default_value=next(iter(TAGS.keys())), + collapsible=True, + class_name="!p-0 w-full !bg-transparent !rounded-none !shadow-none flex flex-col gap-4", + ), + class_name="flex flex-col gap-4", + ) diff --git a/shared/gallery/templates/api-admin-panel.md b/shared/gallery/templates/api-admin-panel.md new file mode 100644 index 0000000..97d796b --- /dev/null +++ b/shared/gallery/templates/api-admin-panel.md @@ -0,0 +1,34 @@ +--- +title: api_admin_panel +description: "Interactive dashboard for API requests and response visualization" +author: "Reflex" +image: "api-admin-panel.webp" +demo: "https://api-admin-panel.reflex.run/" +source: "https://github.com/reflex-dev/templates/tree/main/api_admin_panel" +meta: [ + {"name": "keywords", "content": "admin panel, api admin panel, reflex admin panel"}, +] +tags: ["API Tools"] +--- + +The following is an admin panel for reading from and writing to your customer data, built on a REST API. This app lets you look through customers and take custom actions based on the data. + +## Setup + +To run this app locally, install Reflex and run: + +```bash +reflex init --template api_admin_panel +``` + +To run the app, use: + +```bash +reflex run +``` + +## Usage + +To use the app insert the desired endpoint click `New Request` then in the input field and click on the `Send` button. You can optionally add a body, headers, and cookies to the request. The response will be displayed in the table. + +When clicking on a row the request and response will be displayed in the respective sections. You can further customize this app by adding custom actions to the rows and `Commit` and `Close` buttons. \ No newline at end of file diff --git a/shared/gallery/templates/chat-app.md b/shared/gallery/templates/chat-app.md new file mode 100644 index 0000000..a3e189f --- /dev/null +++ b/shared/gallery/templates/chat-app.md @@ -0,0 +1,42 @@ +--- +title: reflex-chat +description: "Real-time chat application with multiple rooms using Reflex and ChatGPT" +author: "Reflex" +image: "chat-app.webp" +demo: "https://chat.reflex.run/" +source: "https://github.com/reflex-dev/reflex-chat" +meta: [ + {"name": "keywords", "content": ""}, +] +tags: ["AI/ML", "Chat"] +--- +# Chat App + +The following is a python chat app. It is 100% Python-based, including the UI, all using Reflex. Easily create and delete chat sessions. The application is fully customizable and no knowledge of web dev is required to use it and it has responsive design for various devices. + +## Usage + +To run this app locally, install Reflex and run: + +```bash +reflex init --template reflex-chat +``` + +Set up your OpenAI API key: +```bash +export OPENAI_API_KEY=your-openai-api-key +``` + +Install the dependencies and run the app: + +```bash +pip install -r requirements.txt +``` + +```bash +reflex run +``` + +## Customizing the Inference + +You can customize the app by modifying the `chat/state.py` file replacing `model = self.openai_process_question` with that of other LLM providers and writing your own process question function. diff --git a/shared/gallery/templates/ci-job.md b/shared/gallery/templates/ci-job.md new file mode 100644 index 0000000..c877620 --- /dev/null +++ b/shared/gallery/templates/ci-job.md @@ -0,0 +1,30 @@ +--- +title: ci_template +description: "CI/CD job dashboard with real-time updates and controls" +author: "Reflex" +image: "cijob.webp" +demo: "https://cijob.reflex.run/" +source: "https://github.com/reflex-dev/templates/tree/main/ci_template" + +meta: [ + {"name": "keywords", "content": ""}, +] +tags: ["DevOps"] +--- + +In this example we will build a simple CI/CD job dashboard with real-time updates and controls. You can run, edit, and delete jobs on the dashboard as well as view the status of each job. + +## Usage + +To run this app locally, install Reflex and run: + +```bash +reflex init --template ci_template +``` + +To run the app, use: + +```bash +pip install -r requirements.txt +reflex run +``` diff --git a/shared/gallery/templates/customer-app.md b/shared/gallery/templates/customer-app.md new file mode 100644 index 0000000..06641af --- /dev/null +++ b/shared/gallery/templates/customer-app.md @@ -0,0 +1,50 @@ +--- +title: customer_data_app +description: "A Reflex app for customer data management with visualizations" +author: "Reflex" +image: "customer-app.webp" +demo: "https://customer-data-app.reflex.run/" +source: "https://github.com/reflex-dev/templates/tree/main/customer_data_app" +meta: [ + {"name": "keywords", "content": ""}, +] +tags: ["Data Visualization"] +--- + +The following is a python dashboard to interactively display some data, i.e. customer data. The app allows you to add, edit, and delete customer data in a table, as well as visualize the changes in data over time. All the data is stored in a database. It is a good starting point for building more complex apps that require data visualization and editing. + +## Setup + +To run this app locally, install Reflex and run: + +```bash +reflex init --template customer_data_app +``` + +To run the app, use: + +```bash +pip install -r requirements.txt +reflex db migrate +reflex run +``` + + +## Setting an external Database + +It is also possible to set an external database so that your data is not lost every time the app closes and so you can deploy your app and maintain data. + +In the `rxconfig.py` file we accept a `DATABASE_URL` environment variable. + +To set one run the following command in your terminal: + +```bash +export DATABASE_URL="" +``` + + +## Customizing the Database Model + +We define our `Customer` model in the `customer_data_app/customer_data_app/backend/backend.py` file. The model is used to store customer data in the database. You can customize the model to input your own data here. + +It will also be necessary to edit some of the event handlers inside of `State` in the same file and to edit some of the UI components in `customer_data_app/customer_data_app/views/table.py` to reflect the changes in the model. diff --git a/shared/gallery/templates/dalle.md b/shared/gallery/templates/dalle.md new file mode 100644 index 0000000..2dbb570 --- /dev/null +++ b/shared/gallery/templates/dalle.md @@ -0,0 +1,37 @@ +--- +title: dalle +description: "DALL-E is a Reflex app for generating images using OpenAI's API" +author: "Reflex" +image: "dalle.webp" +demo: "https://dalle.reflex.run/" +source: "https://github.com/reflex-dev/templates/tree/main/dalle" +meta: [ + {"name": "keywords", "content": ""}, +] +template: "dalle" +tags: ["AI/ML", "Image Generation"] +--- + +In this example we create a simple app for generating images using OpenAI's API. + +## Usage + +To run this app locally, install Reflex and run: + +```bash +reflex init --template dalle +``` + +Set up your OpenAI API key: +```bash +export OPEN_AI_KEY=your-openai-api-key +``` + +Install the dependencies and run the app: + +```bash +pip install -r requirements.txt +reflex run +``` + + diff --git a/shared/gallery/templates/dashboard.md b/shared/gallery/templates/dashboard.md new file mode 100644 index 0000000..1f01ef4 --- /dev/null +++ b/shared/gallery/templates/dashboard.md @@ -0,0 +1,47 @@ +--- +title: dashboard +description: "Interactive dashboard with real-time data visualization" +author: "Reflex" +image: "dashboard.webp" +demo: "https://dashboard-new.reflex.run/" +source: "https://github.com/reflex-dev/templates/tree/main/dashboard" +meta: [ + {"name": "keywords", "content": ""}, +] +tags: ["Dashboard", "Data Visualization"] +--- + +The following is a dashboard to interactively display data some data. It is a good starting point for building more complex apps that require data visualization. + +## Setup + +To run this app locally, install Reflex and run: + +```bash +reflex init --template dashboard +``` + +To run the app, use: + +```bash +reflex run +``` + +## Customizing to your data + +Right now the apps reads from a local CSV file. You can modify this by changing the `DATA_FILE` variable in the `dashboard/dashboard/backend/table_state.py` file. + +Additionally you will want to change the `Item` class to match the data in your CSV file. + +```python +import dataclasses + +@dataclasses.dataclass +class Item: + """The item class.""" + + name: str + payment: float + date: str + status: str +``` diff --git a/shared/gallery/templates/image-gen.md b/shared/gallery/templates/image-gen.md new file mode 100644 index 0000000..ed8d7d4 --- /dev/null +++ b/shared/gallery/templates/image-gen.md @@ -0,0 +1,41 @@ +--- +title: ai_image_gen +description: "Generate AI images using Replicate's API" +author: "Reflex" +image: "image-gen.webp" +demo: "https://ai-image-gen.reflex.run/" +source: "https://github.com/reflex-dev/templates/tree/main/ai_image_gen" +meta: [ + {"name": "keywords", "content": "image generation, ai image generation, reflex image generation, Replicate image generation"}, +] +tags: ["AI/ML", "Image Generation"] +--- + +The following is an app that allows you to generate AI images. The current map uses replicate's api to generate images but can be easily modified to use other image generation services. + +## Setup + +To run this app locally, install Reflex and run: + +```bash +reflex init --template ai_image_gen +``` + +To run the app, set the `REPLICATE_API_TOKEN`: + +```bash +export REPLICATE_API_TOKEN=your_api_token_here +``` + +Then run: + +```bash +pip install -r requirements.txt +reflex run +``` + +Note: You can get your replicate api token [here](https://replicate.com/account/api-tokens). + +## Customizing the Inference + +You can customize the app by modifying the [`generation.py`](https://github.com/reflex-dev/templates/blob/main/ai_image_gen/ai_image_gen/backend/generation.py) file replacing replicate's api with that of other image generation services. \ No newline at end of file diff --git a/shared/gallery/templates/llamaindex-app.md b/shared/gallery/templates/llamaindex-app.md new file mode 100644 index 0000000..d6c43d3 --- /dev/null +++ b/shared/gallery/templates/llamaindex-app.md @@ -0,0 +1,68 @@ +--- +title: reflex-llamaindex-template +description: "A minimal chat app using LLamaIndex" +author: "Reflex" +image: "llamaindex.png" +source: "https://github.com/reflex-dev/reflex-llamaindex-template" +meta: [ + {"name": "keywords", "content": ""}, +] +tags: ["AI/ML", "Chat"] +--- + +The following is an alternative UI to display the LLamaIndex app. + +## Prerequisites + +If you plan on deploying your agentic workflow to prod, follow the [llama deploy tutorial](https://github.com/run-llama/llama_deploy/tree/main) to deploy your agentic workflow. + +## Setup + +To run this app locally, install Reflex and run: + +```bash +reflex init --template reflex-llamaindex-template +``` + + + +The following [lines](https://github.com/reflex-dev/reflex-llamaindex-template/blob/abfda49ff193ceb7da90c382e5cbdcb5fcdb665c/frontend/state.py#L55-L79) in the state.py file are where the app makes a request to your deployed agentic workflow. If you have not deployed your agentic workflow, you can edit this to call and api endpoint of your choice. + +```python +client = httpx.AsyncClient() + +# call the agentic workflow +input_payload = { + "chat_history_dicts": chat_history_dicts, + "user_input": question, +} +deployment_name = os.environ.get("DEPLOYMENT_NAME", "MyDeployment") +apiserver_url = os.environ.get("APISERVER_URL", "http://localhost:4501") +response = await client.post( + f"\{apiserver_url}/deployments/\{deployment_name}/tasks/create", + json=\{"input": json.dumps(input_payload)}, + timeout=60, +) +answer = response.text + +for i in range(len(answer)): + # Pause to show the streaming effect. + await asyncio.sleep(0.01) + # Add one letter at a time to the output. + self.chat_history[-1] = ( + self.chat_history[-1][0], + answer[: i + 1], + ) + yield +``` + +### Run the app + +Once you have set up your environment, install the dependencies and run the app: + +```bash +cd reflex-llamaindex-template +pip install -r requirements.txt +reflex run +``` + diff --git a/shared/gallery/templates/nba-app.md b/shared/gallery/templates/nba-app.md new file mode 100644 index 0000000..acefcb8 --- /dev/null +++ b/shared/gallery/templates/nba-app.md @@ -0,0 +1,29 @@ +--- +title: nba +description: "Interactive NBA app with player stats and live updates" +author: "Reflex" +image: "nba-app.webp" +demo: "https://nba-new.reflex.run/" +source: "https://github.com/reflex-dev/templates/tree/main/nba" +meta: [ + {"name": "keywords", "content": ""}, +] +template: "nba" +tags: ["Sports", "Data Visualization"] +--- + +The following is an app that displays NBA player stats from the 2015-2016 season. The table tab allows filtering and live updates. The graph tab shows the relationship between player stats. + +## Setup + +To run this app locally, install Reflex and run: + +```bash +reflex init --template nba +``` + +To run the app, use: + +```bash +reflex run +``` \ No newline at end of file diff --git a/shared/gallery/templates/sales-app.md b/shared/gallery/templates/sales-app.md new file mode 100644 index 0000000..bb860bf --- /dev/null +++ b/shared/gallery/templates/sales-app.md @@ -0,0 +1,40 @@ +--- +title: sales +description: "Sales app with interactive charts and real-time data updates" +author: "Reflex" +image: "sales.webp" +demo: "https://sales-new.reflex.run/" +source: "https://github.com/reflex-dev/templates/tree/main/sales" +meta: [ + {"name": "keywords", "content": ""}, +] +tags: ["Marketing", "Dashboard"] +--- + +The following is a sales app that displays sales data. The table tab allows filtering and live updates. The graph tab shows the relationship between sales data. + +## Setup + +To run this app locally, install Reflex and run: + +```bash +reflex init --template sales +``` + +To run the app, use: + +Set the OpenAI API key: +``` +export OPEN_AI_KEY=your-openai-api-key +``` + +```bash +pip install -r requirements.txt +reflex run +``` + +## Customizing the Inference + +Note: You can get your OpenAI API key [here](https://platform.openai.com/account/api-keys). + +You can customize the app by modifying the `sales/sales/backend/backend.py` file replacing OpenAI's API with that of other LLM providers. \ No newline at end of file diff --git a/shared/lib/__init__.py b/shared/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/shared/lib/meta/__init__.py b/shared/lib/meta/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/shared/lib/meta/meta.py b/shared/lib/meta/meta.py new file mode 100644 index 0000000..326868c --- /dev/null +++ b/shared/lib/meta/meta.py @@ -0,0 +1,330 @@ +import json + +import reflex as rx + +from shared.constants import ( + DISCORD_URL, + FORUM_URL, + GITHUB_URL, + LINKEDIN_URL, + REFLEX_ASSETS_CDN, + REFLEX_DOMAIN, + REFLEX_DOMAIN_URL, + TWITTER_CREATOR, + TWITTER_URL, +) + +TITLE = "The unified platform to build and scale enterprise apps." +ONE_LINE_DESCRIPTION = "Build with AI, iterate in Python, deploy to any cloud. Reflex is the platform for full-stack web apps and internal tools." + +# Common constants +APPLICATION_NAME = "Reflex" +TWITTER_CARD_TYPE = "summary_large_image" +OG_TYPE = "website" + + +def _build_meta_tags( + title: str, + description: str, + image: str, + url: str = REFLEX_DOMAIN_URL, +) -> list[dict[str, str]]: + """Build a list of meta tags with the given parameters. + + Args: + title: The page title. + description: The page description. + image: The image path for social media previews. + url: The page URL (defaults to REFLEX_DOMAIN_URL). + + Returns: + A list of meta tag dictionaries. + """ + return [ + # HTML Meta Tags + {"name": "application-name", "content": APPLICATION_NAME}, + {"name": "description", "content": description}, + # Facebook Meta Tags + {"property": "og:url", "content": url}, + {"property": "og:type", "content": OG_TYPE}, + {"property": "og:title", "content": title}, + {"property": "og:description", "content": description}, + {"property": "og:image", "content": image}, + # Twitter Meta Tags + {"name": "twitter:card", "content": TWITTER_CARD_TYPE}, + {"property": "twitter:domain", "content": REFLEX_DOMAIN}, + {"property": "twitter:url", "content": url}, + {"name": "twitter:title", "content": title}, + {"name": "twitter:description", "content": description}, + {"name": "twitter:image", "content": image}, + {"name": "twitter:creator", "content": TWITTER_CREATOR}, + ] + + +meta_tags = _build_meta_tags( + title=TITLE, + description=ONE_LINE_DESCRIPTION, + image=f"{REFLEX_ASSETS_CDN}previews/index_preview.webp", +) + +hosting_meta_tags = _build_meta_tags( + title=TITLE, + description=ONE_LINE_DESCRIPTION, + image=f"{REFLEX_ASSETS_CDN}previews/hosting_preview.webp", +) + + +def favicons_links() -> list[dict[str, str] | rx.Component]: + return [ + rx.el.link( + rel="apple-touch-icon", sizes="180x180", href="/meta/apple-touch-icon.png" + ), + rx.el.link( + rel="icon", type="image/png", sizes="32x32", href="/meta/favicon-32x32.png" + ), + rx.el.link( + rel="icon", type="image/png", sizes="16x16", href="/meta/favicon-16x16.png" + ), + rx.el.link(rel="manifest", href="/meta/site.webmanifest"), + rx.el.link(rel="shortcut icon", href="/favicon.ico"), + ] + + +def to_cdn_image_url(image: str | None) -> str: + """Convert a relative image path to a full CDN URL. + + Root-level paths (e.g. /reflex_banner.png) map to other/ on the CDN. + Paths with subfolders (e.g. /blog/on-prem.webp) map 1:1. + """ + if not image or image.startswith(("http://", "https://")): + return image or "" + path = image.lstrip("/") if image.startswith("/") else image + if "/" not in path: + path = f"other/{path}" + return f"{REFLEX_ASSETS_CDN}{path}" + + +def create_meta_tags( + title: str, description: str, image: str, url: str | None = None +) -> list[dict[str, str] | rx.Component]: + """Create meta tags for a page. + + Args: + title: The page title. + description: The page description. + image: The image path for social media previews. + url: The page URL (optional, defaults to REFLEX_DOMAIN_URL). + + Returns: + A list of meta tag dictionaries. + """ + page_url = url or REFLEX_DOMAIN_URL + image_url = to_cdn_image_url(image) if image else "" + + return [ + *_build_meta_tags( + title=title, + description=description, + image=image_url, + url=page_url, + ), + rx.el.link(rel="canonical", href=page_url), + ] + + +def blog_jsonld( + title: str, + description: str, + author: str, + date: str, + image: str, + url: str, + faq: list[dict[str, str]] | None = None, + author_bio: str | None = None, + updated_at: str | None = None, + word_count: int | None = None, + keywords: list[str] | None = None, +) -> rx.Component: + """Create a single JSON-LD script tag with @graph for a blog post. + + Always includes a BlogPosting entry. If faq items are provided, + a FAQPage entry is also added to the graph. + """ + author_node: dict = {"@type": "Person", "name": author} + if author_bio: + author_node["description"] = author_bio + + posting: dict = { + "@type": "BlogPosting", + "headline": title, + "description": description, + "image": to_cdn_image_url(image), + "datePublished": str(date), + "url": url, + "author": author_node, + } + if updated_at: + posting["dateModified"] = str(updated_at) + if word_count: + posting["wordCount"] = word_count + if keywords: + posting["keywords"] = keywords + + graph: list[dict] = [ + { + **posting, + "publisher": { + "@type": "Organization", + "name": "Reflex", + "url": REFLEX_DOMAIN_URL, + }, + "mainEntityOfPage": { + "@type": "WebPage", + "@id": url, + }, + }, + ] + if faq: + graph.append( + { + "@type": "FAQPage", + "mainEntity": [ + { + "@type": "Question", + "name": item["question"], + "acceptedAnswer": { + "@type": "Answer", + "text": item["answer"], + }, + } + for item in faq + ], + } + ) + data = { + "@context": "https://schema.org", + "@graph": graph, + } + return rx.el.script(json.dumps(data), type="application/ld+json") + + +def website_organization_jsonld(url: str = REFLEX_DOMAIN_URL) -> rx.Component: + """Create Organization + WebSite JSON-LD for the homepage.""" + org_url = REFLEX_DOMAIN_URL.rstrip("/") + data = { + "@context": "https://schema.org", + "@graph": [ + { + "@type": "Organization", + "@id": f"{org_url}/#organization", + "name": "Reflex", + "url": REFLEX_DOMAIN_URL, + "logo": f"{org_url}/meta/apple-touch-icon.png", + "description": "Open-source Python framework for building full-stack web applications. Deploy to any cloud with AI-powered code generation.", + "sameAs": [ + GITHUB_URL, + TWITTER_URL, + DISCORD_URL, + LINKEDIN_URL, + FORUM_URL, + ], + }, + { + "@type": "WebSite", + "name": "Reflex", + "url": url, + "description": ONE_LINE_DESCRIPTION, + "publisher": {"@id": f"{org_url}/#organization"}, + }, + ], + } + return rx.el.script(json.dumps(data), type="application/ld+json") + + +def blog_index_jsonld(posts: list[tuple[str, dict]], url: str) -> rx.Component: + """Create Blog JSON-LD with ItemList of posts for the blog index page.""" + items = [ + { + "@type": "ListItem", + "position": i + 1, + "url": f"{REFLEX_DOMAIN_URL.rstrip('/')}/blog/{path}", + "name": meta.get("title_tag") or meta.get("title", ""), + "datePublished": str(meta.get("date", "")), + } + for i, (path, meta) in enumerate(posts[:20]) + ] + blog_posts = [ + { + "@type": "BlogPosting", + "headline": meta.get("title_tag") or meta.get("title", ""), + "url": f"{REFLEX_DOMAIN_URL.rstrip('/')}/blog/{path}", + "datePublished": str(meta.get("date", "")), + } + for path, meta in posts[:20] + ] + data = { + "@context": "https://schema.org", + "@graph": [ + { + "@type": "Blog", + "name": "Reflex Blog", + "description": "Python web app tutorials, framework comparisons, and tips for building with Reflex.", + "url": url, + "publisher": { + "@type": "Organization", + "name": "Reflex", + "url": REFLEX_DOMAIN_URL, + }, + "blogPost": blog_posts, + }, + { + "@type": "ItemList", + "itemListElement": items, + "numberOfItems": len(items), + }, + ], + } + return rx.el.script(json.dumps(data), type="application/ld+json") + + +def faq_jsonld(faq_schema: dict) -> rx.Component: + """Create a FAQPage JSON-LD script tag from a pre-built schema dict.""" + return rx.el.script(json.dumps(faq_schema), type="application/ld+json") + + +def pricing_jsonld(url: str) -> rx.Component: + """Create SoftwareApplication + Product JSON-LD for the pricing page.""" + data = { + "@context": "https://schema.org", + "@graph": [ + { + "@type": "SoftwareApplication", + "name": "Reflex", + "applicationCategory": "DeveloperApplication", + "description": "The platform to build and scale enterprise apps. Python full-stack framework for web apps and internal tools.", + "url": url, + }, + { + "@type": "Product", + "name": "Reflex Enterprise Platform", + "brand": {"@type": "Brand", "name": "Reflex"}, + "description": "Enterprise-grade fullstack app building platform with AI-powered code generation in pure Python. Includes dedicated support, SSO, on-prem deployment, and custom SLAs.", + "offers": [ + { + "@type": "Offer", + "price": "0", + "priceCurrency": "USD", + "name": "Free", + "availability": "https://schema.org/InStock", + }, + { + "@type": "Offer", + "name": "Enterprise", + "description": "Custom enterprise pricing", + "availability": "https://schema.org/PreOrder", + }, + ], + }, + ], + } + return rx.el.script(json.dumps(data), type="application/ld+json") diff --git a/shared/lib/route.py b/shared/lib/route.py new file mode 100644 index 0000000..6e9f16b --- /dev/null +++ b/shared/lib/route.py @@ -0,0 +1,58 @@ +"""Manage routing for the application.""" + +import dataclasses +import inspect +from collections.abc import Callable + +import reflex as rx +from reflex.event import EventType + + +@dataclasses.dataclass(kw_only=True) +class Route: + """A page route.""" + + # The path of the route. + path: str + + # The page title. + title: str | rx.Var | None = None + + # The page description. + description: str | None = None + + # The page image. + image: str | None = None + + # The page extra meta data. + meta: list[dict[str, str]] | None = None + + # Background color for the page. + background_color: str | None = None + + # The component to render for the route. + component: Callable[[], rx.Component] + + # whether to add the route to the app's pages. This is typically used + # to delay adding the 404 page(which is explicitly added in reflex_blog.py). + # https://github.com/reflex-dev/reflex-web/pull/659#pullrequestreview-2021171902 + add_as_page: bool = True + + # The on_load function to call when the page is loaded. + on_load: EventType[()] | None = None + + +def get_path(component_fn: Callable, route: str): + """Get the path for a page based on the file location. + + Args: + component_fn: The component function for the page. + route: The route path for the page. + """ + module = inspect.getmodule(component_fn) + if module is None: + msg = f"Could not find module for {component_fn}" + raise ValueError(msg) + + # Create a path based on the module name. + return module.__name__.replace(".", "/").replace("_", "-").split(route)[1] + "/" diff --git a/shared/meta/__init__.py b/shared/meta/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/shared/meta/meta.py b/shared/meta/meta.py new file mode 100644 index 0000000..326868c --- /dev/null +++ b/shared/meta/meta.py @@ -0,0 +1,330 @@ +import json + +import reflex as rx + +from shared.constants import ( + DISCORD_URL, + FORUM_URL, + GITHUB_URL, + LINKEDIN_URL, + REFLEX_ASSETS_CDN, + REFLEX_DOMAIN, + REFLEX_DOMAIN_URL, + TWITTER_CREATOR, + TWITTER_URL, +) + +TITLE = "The unified platform to build and scale enterprise apps." +ONE_LINE_DESCRIPTION = "Build with AI, iterate in Python, deploy to any cloud. Reflex is the platform for full-stack web apps and internal tools." + +# Common constants +APPLICATION_NAME = "Reflex" +TWITTER_CARD_TYPE = "summary_large_image" +OG_TYPE = "website" + + +def _build_meta_tags( + title: str, + description: str, + image: str, + url: str = REFLEX_DOMAIN_URL, +) -> list[dict[str, str]]: + """Build a list of meta tags with the given parameters. + + Args: + title: The page title. + description: The page description. + image: The image path for social media previews. + url: The page URL (defaults to REFLEX_DOMAIN_URL). + + Returns: + A list of meta tag dictionaries. + """ + return [ + # HTML Meta Tags + {"name": "application-name", "content": APPLICATION_NAME}, + {"name": "description", "content": description}, + # Facebook Meta Tags + {"property": "og:url", "content": url}, + {"property": "og:type", "content": OG_TYPE}, + {"property": "og:title", "content": title}, + {"property": "og:description", "content": description}, + {"property": "og:image", "content": image}, + # Twitter Meta Tags + {"name": "twitter:card", "content": TWITTER_CARD_TYPE}, + {"property": "twitter:domain", "content": REFLEX_DOMAIN}, + {"property": "twitter:url", "content": url}, + {"name": "twitter:title", "content": title}, + {"name": "twitter:description", "content": description}, + {"name": "twitter:image", "content": image}, + {"name": "twitter:creator", "content": TWITTER_CREATOR}, + ] + + +meta_tags = _build_meta_tags( + title=TITLE, + description=ONE_LINE_DESCRIPTION, + image=f"{REFLEX_ASSETS_CDN}previews/index_preview.webp", +) + +hosting_meta_tags = _build_meta_tags( + title=TITLE, + description=ONE_LINE_DESCRIPTION, + image=f"{REFLEX_ASSETS_CDN}previews/hosting_preview.webp", +) + + +def favicons_links() -> list[dict[str, str] | rx.Component]: + return [ + rx.el.link( + rel="apple-touch-icon", sizes="180x180", href="/meta/apple-touch-icon.png" + ), + rx.el.link( + rel="icon", type="image/png", sizes="32x32", href="/meta/favicon-32x32.png" + ), + rx.el.link( + rel="icon", type="image/png", sizes="16x16", href="/meta/favicon-16x16.png" + ), + rx.el.link(rel="manifest", href="/meta/site.webmanifest"), + rx.el.link(rel="shortcut icon", href="/favicon.ico"), + ] + + +def to_cdn_image_url(image: str | None) -> str: + """Convert a relative image path to a full CDN URL. + + Root-level paths (e.g. /reflex_banner.png) map to other/ on the CDN. + Paths with subfolders (e.g. /blog/on-prem.webp) map 1:1. + """ + if not image or image.startswith(("http://", "https://")): + return image or "" + path = image.lstrip("/") if image.startswith("/") else image + if "/" not in path: + path = f"other/{path}" + return f"{REFLEX_ASSETS_CDN}{path}" + + +def create_meta_tags( + title: str, description: str, image: str, url: str | None = None +) -> list[dict[str, str] | rx.Component]: + """Create meta tags for a page. + + Args: + title: The page title. + description: The page description. + image: The image path for social media previews. + url: The page URL (optional, defaults to REFLEX_DOMAIN_URL). + + Returns: + A list of meta tag dictionaries. + """ + page_url = url or REFLEX_DOMAIN_URL + image_url = to_cdn_image_url(image) if image else "" + + return [ + *_build_meta_tags( + title=title, + description=description, + image=image_url, + url=page_url, + ), + rx.el.link(rel="canonical", href=page_url), + ] + + +def blog_jsonld( + title: str, + description: str, + author: str, + date: str, + image: str, + url: str, + faq: list[dict[str, str]] | None = None, + author_bio: str | None = None, + updated_at: str | None = None, + word_count: int | None = None, + keywords: list[str] | None = None, +) -> rx.Component: + """Create a single JSON-LD script tag with @graph for a blog post. + + Always includes a BlogPosting entry. If faq items are provided, + a FAQPage entry is also added to the graph. + """ + author_node: dict = {"@type": "Person", "name": author} + if author_bio: + author_node["description"] = author_bio + + posting: dict = { + "@type": "BlogPosting", + "headline": title, + "description": description, + "image": to_cdn_image_url(image), + "datePublished": str(date), + "url": url, + "author": author_node, + } + if updated_at: + posting["dateModified"] = str(updated_at) + if word_count: + posting["wordCount"] = word_count + if keywords: + posting["keywords"] = keywords + + graph: list[dict] = [ + { + **posting, + "publisher": { + "@type": "Organization", + "name": "Reflex", + "url": REFLEX_DOMAIN_URL, + }, + "mainEntityOfPage": { + "@type": "WebPage", + "@id": url, + }, + }, + ] + if faq: + graph.append( + { + "@type": "FAQPage", + "mainEntity": [ + { + "@type": "Question", + "name": item["question"], + "acceptedAnswer": { + "@type": "Answer", + "text": item["answer"], + }, + } + for item in faq + ], + } + ) + data = { + "@context": "https://schema.org", + "@graph": graph, + } + return rx.el.script(json.dumps(data), type="application/ld+json") + + +def website_organization_jsonld(url: str = REFLEX_DOMAIN_URL) -> rx.Component: + """Create Organization + WebSite JSON-LD for the homepage.""" + org_url = REFLEX_DOMAIN_URL.rstrip("/") + data = { + "@context": "https://schema.org", + "@graph": [ + { + "@type": "Organization", + "@id": f"{org_url}/#organization", + "name": "Reflex", + "url": REFLEX_DOMAIN_URL, + "logo": f"{org_url}/meta/apple-touch-icon.png", + "description": "Open-source Python framework for building full-stack web applications. Deploy to any cloud with AI-powered code generation.", + "sameAs": [ + GITHUB_URL, + TWITTER_URL, + DISCORD_URL, + LINKEDIN_URL, + FORUM_URL, + ], + }, + { + "@type": "WebSite", + "name": "Reflex", + "url": url, + "description": ONE_LINE_DESCRIPTION, + "publisher": {"@id": f"{org_url}/#organization"}, + }, + ], + } + return rx.el.script(json.dumps(data), type="application/ld+json") + + +def blog_index_jsonld(posts: list[tuple[str, dict]], url: str) -> rx.Component: + """Create Blog JSON-LD with ItemList of posts for the blog index page.""" + items = [ + { + "@type": "ListItem", + "position": i + 1, + "url": f"{REFLEX_DOMAIN_URL.rstrip('/')}/blog/{path}", + "name": meta.get("title_tag") or meta.get("title", ""), + "datePublished": str(meta.get("date", "")), + } + for i, (path, meta) in enumerate(posts[:20]) + ] + blog_posts = [ + { + "@type": "BlogPosting", + "headline": meta.get("title_tag") or meta.get("title", ""), + "url": f"{REFLEX_DOMAIN_URL.rstrip('/')}/blog/{path}", + "datePublished": str(meta.get("date", "")), + } + for path, meta in posts[:20] + ] + data = { + "@context": "https://schema.org", + "@graph": [ + { + "@type": "Blog", + "name": "Reflex Blog", + "description": "Python web app tutorials, framework comparisons, and tips for building with Reflex.", + "url": url, + "publisher": { + "@type": "Organization", + "name": "Reflex", + "url": REFLEX_DOMAIN_URL, + }, + "blogPost": blog_posts, + }, + { + "@type": "ItemList", + "itemListElement": items, + "numberOfItems": len(items), + }, + ], + } + return rx.el.script(json.dumps(data), type="application/ld+json") + + +def faq_jsonld(faq_schema: dict) -> rx.Component: + """Create a FAQPage JSON-LD script tag from a pre-built schema dict.""" + return rx.el.script(json.dumps(faq_schema), type="application/ld+json") + + +def pricing_jsonld(url: str) -> rx.Component: + """Create SoftwareApplication + Product JSON-LD for the pricing page.""" + data = { + "@context": "https://schema.org", + "@graph": [ + { + "@type": "SoftwareApplication", + "name": "Reflex", + "applicationCategory": "DeveloperApplication", + "description": "The platform to build and scale enterprise apps. Python full-stack framework for web apps and internal tools.", + "url": url, + }, + { + "@type": "Product", + "name": "Reflex Enterprise Platform", + "brand": {"@type": "Brand", "name": "Reflex"}, + "description": "Enterprise-grade fullstack app building platform with AI-powered code generation in pure Python. Includes dedicated support, SSO, on-prem deployment, and custom SLAs.", + "offers": [ + { + "@type": "Offer", + "price": "0", + "priceCurrency": "USD", + "name": "Free", + "availability": "https://schema.org/InStock", + }, + { + "@type": "Offer", + "name": "Enterprise", + "description": "Custom enterprise pricing", + "availability": "https://schema.org/PreOrder", + }, + ], + }, + ], + } + return rx.el.script(json.dumps(data), type="application/ld+json") diff --git a/shared/pages/__init__.py b/shared/pages/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/shared/pages/page404.py b/shared/pages/page404.py new file mode 100644 index 0000000..9fa822a --- /dev/null +++ b/shared/pages/page404.py @@ -0,0 +1,18 @@ +import reflex as rx + +from shared.components.blocks.flexdown import markdown_with_shiki +from shared.templates.webpage import webpage + +contents = f""" +# Page Not Found + +The page at `{rx.State.router.page.raw_path}` doesn't exist. +""" + + +@webpage(path="/404", title="Page Not Found · Reflex.dev", add_as_page=False) +def page404(): + return rx.box( + markdown_with_shiki(contents), + class_name="h-[80vh] w-full flex flex-col items-center justify-center", + ) diff --git a/shared/route.py b/shared/route.py new file mode 100644 index 0000000..f12d34a --- /dev/null +++ b/shared/route.py @@ -0,0 +1,5 @@ +"""Re-export route utilities from shared.lib.route.""" + +from shared.lib.route import Route, get_path + +__all__ = ["Route", "get_path"] diff --git a/shared/styles/__init__.py b/shared/styles/__init__.py new file mode 100644 index 0000000..a1e7b0b --- /dev/null +++ b/shared/styles/__init__.py @@ -0,0 +1 @@ +from .styles import * diff --git a/shared/styles/colors.py b/shared/styles/colors.py new file mode 100644 index 0000000..438857a --- /dev/null +++ b/shared/styles/colors.py @@ -0,0 +1,11 @@ +# Customs colors from /assets/custom-colors.css +from typing import Literal + +ColorType = Literal["white", "slate", "violet", "jade", "red"] +ShadeType = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] + + +def c_color(color: ColorType, shade: ShadeType, alpha: bool = False) -> str: + """Create a color variable string.""" + shade_str = str(shade).replace(".", "-") + return f"var(--c-{color}-{shade_str}{'-a' if alpha else ''})" diff --git a/shared/styles/custom-colors.css b/shared/styles/custom-colors.css new file mode 100644 index 0000000..b8ced52 --- /dev/null +++ b/shared/styles/custom-colors.css @@ -0,0 +1,182 @@ +/* Custom colors */ + +.light, +.light-theme { + /* Slate */ + --c-slate-1: #FCFCFD; + --c-slate-2: #F9F9FB; + --c-slate-3: #F0F0F3; + --c-slate-4: #E8E8EC; + --c-slate-5: #E0E1E6; + --c-slate-6: #D9D9E0; + --c-slate-7: #CDCED6; + --c-slate-8: #B9BBC6; + --c-slate-9: #8B8D98; + --c-slate-10: #80838D; + --c-slate-11: #60646C; + --c-slate-12: #1C2024; + --slate-1: #FBFCFE; + --slate-2: #F7FAFD; + --slate-3: #EDF1F6; + --slate-4: #E4E9F1; + --slate-5: #DBE1EB; + --slate-6: #D3DAE6; + --slate-7: #C7D0DE; + --slate-8: #B1BDCF; + --slate-9: #838FA1; + --slate-10: #798495; + --slate-11: #5C6573; + --slate-12: #1B212A; + --slate-a1: #0040c004; + --slate-a2: #0040c008; + --slate-a3: #00398e12; + --slate-a4: #0037801c; + --slate-a5: #00327924; + --slate-a6: #002e772d; + --slate-a7: #002d7039; + --slate-a8: #0028634e; + --slate-a9: #00193c7c; + --slate-a10: #00153686; + --slate-a11: #001027a4; + --slate-a12: #000711e5; + /* Violet */ + --c-violet-1: #FDFCFE; + --c-violet-2: #FAFBFF; + --c-violet-3: #F4F0FE; + --c-violet-4: #EBE4FF; + --c-violet-5: #E1D9FF; + --c-violet-6: #D4CAFE; + --c-violet-7: #C2B5F5; + --c-violet-8: #AA99EC; + --c-violet-9: #6E56CF; + --c-violet-10: #654DC4; + --c-violet-11: #6550B9; + --c-violet-12: #2F265F; + /* Jade */ + --c-jade-8: #56BA9F; + --c-jade-8-a: rgba(86, 186, 159, 0.16); + /* Red */ + --c-red-9: #E5484D; + --c-red-10: #DC3E42; + /* White */ + --c-white-1: #FFFFFF; + --glow: #EBE4FF; + --wave-line-1: #D4CAFE; + --wave-line-2: #EBE4FF; + /* Marketing Colors */ + --m-slate-1: #FCFCFD; + --m-slate-2: #F6F7F9; + --m-slate-3: #EEEFF2; + --m-slate-4: #E5E8EB; + --m-slate-5: #CACDD4; + --m-slate-6: #979FAA; + --m-slate-7: #67707E; + --m-slate-8: #3C434E; + --m-slate-9: #2A3037; + --m-slate-10: #21252B; + --m-slate-11: #1D2025; + --m-slate-12: #151618; + --m-slate-13: #1C2024; + --m-slate-14: #1A1B1D; + --m-slate-15: #151618; + --m-violet-1: #FDFCFE; + --m-violet-2: #FAF8FF; + --m-violet-3: #F4F0FE; + --m-violet-4: #EBE4FF; + --m-violet-5: #E1D9FF; + --m-violet-6: #D4CAFE; + --m-violet-7: #C2B5F5; + --m-violet-8: #AA99EC; + --m-violet-9: #6E56CF; + --m-violet-10: #654DC4; + --m-violet-11: #6550B9; + --m-violet-12: #2F265F; +} + +.dark, +.dark-theme { + /* Slate */ + --slate-1: #141619; + --slate-2: #1B1D20; + --slate-3: #22252A; + --slate-4: #282B31; + --slate-5: #2E3238; + --slate-6: #353A42; + --slate-7: #414852; + --slate-8: #58616F; + --slate-9: #656E7D; + --slate-10: #737C8A; + --slate-11: #ADB4BF; + --slate-12: #ECEEF1; + /* #151618 */ + --c-slate-2: #1A1B1D; + /* #1A1B1D */ + --c-slate-3: #222326; + /* #222326 */ + --c-slate-4: #27282B; + /* #27282B */ + --c-slate-5: #303236; + /* #303236 */ + --c-slate-6: #4B4D53; + /* #4B4D53 */ + --c-slate-7: #5E5F69; + /* #5E5F69 */ + --c-slate-8: #6E7287; + /* #6E7287 */ + --c-slate-9: #9A9CAC; + /* #9A9CAC */ + --c-slate-10: #D9D9E0; + /* #D9D9E0 */ + --c-slate-11: #E0E1E6; + /* #E0E1E6 */ + --c-slate-12: #FCFCFD; + /* #FCFCFD */ + /* Violet */ + --c-violet-1: #16112C; + --c-violet-2: #140E2B; + --c-violet-3: #261958; + --c-violet-4: #2F1C78; + --c-violet-5: #352088; + --c-violet-6: #4329AC; + --c-violet-7: #5638D3; + --c-violet-8: #5F43D0; + --c-violet-9: #baa7ff; + --c-violet-10: #D6C8FB; + --c-violet-11: #EDE8FE; + --c-violet-12: #FAF5FE; + /* Jade */ + --c-jade-8: #56BA9F; + --c-jade-8-a: rgba(86, 186, 159, 0.16); + /* Red */ + --c-red-9: #E5484D; + --c-red-10: #DC3E42; + /* White */ + --c-white-1: #1B1D20; + --glow: #261958; + --wave-line-1: #2F1C78; + --wave-line-2: #261958; + --m-slate-1: #FCFCFD; + --m-slate-2: #F6F7F9; + --m-slate-3: #EEEFF2; + --m-slate-4: #E5E8EB; + --m-slate-5: #CACDD4; + --m-slate-6: #979FAA; + --m-slate-7: #67707E; + --m-slate-8: #3C434E; + --m-slate-9: #2A3037; + --m-slate-10: #21252B; + --m-slate-11: #1D2025; + --m-slate-12: #151618; + --m-violet-1: #FDFCFE; + --m-violet-2: #FAF8FF; + --m-violet-3: #F4F0FE; + --m-violet-4: #EBE4FF; + --m-violet-5: #E1D9FF; + --m-violet-6: #D4CAFE; + --m-violet-7: #C2B5F5; + --m-violet-8: #AA99EC; + --m-violet-9: #6E56CF; + --m-violet-10: #654DC4; + --m-violet-11: #6550B9; + --m-violet-12: #2F265F; +} \ No newline at end of file diff --git a/shared/styles/fonts.py b/shared/styles/fonts.py new file mode 100644 index 0000000..07952f5 --- /dev/null +++ b/shared/styles/fonts.py @@ -0,0 +1,94 @@ +# FONT STYLES + +font_family = "Instrument Sans" + +small = { + "font-family": font_family, + "font-style": "normal", + "font-weight": "500", + "font-size": "14px", + "line-height": "20px", + "letter-spacing": "-0.0125em", +} + +small_semibold = { + "font-family": font_family, + "font-size": "14px", + "font-style": "normal", + "font-weight": "600", + "line-height": "20px", + "letter-spacing": "-0.0125em", +} + +base = { + "font-family": font_family, + "font-style": "normal", + "font-weight": "500", + "font-size": ["14px", "16px"], + "line-height": ["20px", "24px"], + "letter-spacing": "-0.015em", +} + +base_semibold = { + "font-family": font_family, + "font-style": "normal", + "font-weight": "600", + "font-size": "16px", + "line-height": "24px", + "letter-spacing": "-0.015em", +} + +medium = { + "font-family": font_family, + "font-style": "normal", + "font-weight": "500", + "font-size": "18px", + "line-height": "26px", + "letter-spacing": "-0.015em", +} + +medium_leading_5 = { + "font-family": font_family, + "font-style": "normal", + "font-weight": "500", + "font-size": "18px", + "line-height": "20px", + "letter-spacing": "-0.015em", +} + +large = { + "font-family": font_family, + "font-style": "normal", + "font-weight": "600", + "font-size": "24px", + "line-height": "32px", + "letter-spacing": "-0.015em", +} + +x_large = { + "font-family": font_family, + "font-style": "normal", + "font-weight": "600", + "font-size": ["24px", "32px"], + "line-height": ["32px", "40px"], + "letter-spacing": ["-0.02em", "-0.03em"], +} + + +xx_large = { + "font-family": font_family, + "font-style": "normal", + "font-weight": "600", + "font-size": ["32px", "56px"], + "line-height": ["40px", "64px"], + "letter-spacing": ["-0.03em", "-0.05em"], +} + +code = { + "font-family": "JetBrains Mono", + "font-size": "14px", + "font-style": "normal", + "font-weight": "400", + "line-height": "24px", + "letter-spacing": "-0.015em", +} diff --git a/shared/styles/globals.css b/shared/styles/globals.css new file mode 100644 index 0000000..2b31897 --- /dev/null +++ b/shared/styles/globals.css @@ -0,0 +1,2051 @@ +@import "tailwindcss-animated"; + +@custom-variant dark (&:where(.dark, .dark *)); + +:root { + /* Primary */ + --primary-1: var(--violet-1); + --primary-2: var(--violet-2); + --primary-3: var(--violet-3); + --primary-4: var(--violet-4); + --primary-5: var(--violet-5); + --primary-6: var(--violet-6); + --primary-7: var(--violet-7); + --primary-8: var(--violet-8); + --primary-9: var(--violet-9); + --primary-10: var(--violet-10); + --primary-11: var(--violet-11); + --primary-12: var(--violet-12); + --primary-a1: var(--violet-a1); + --primary-a2: var(--violet-a2); + --primary-a3: var(--violet-a3); + --primary-a4: var(--violet-a4); + --primary-a5: var(--violet-a5); + --primary-a6: var(--violet-a6); + --primary-a7: var(--violet-a7); + --primary-a8: var(--violet-a8); + --primary-a9: var(--violet-a9); + --primary-a10: var(--violet-a10); + --primary-a11: var(--violet-a11); + --primary-a12: var(--violet-a12); + /* Contrast */ + --primary-contrast: var(--violet-contrast); + /* Secondary */ + --secondary-1: var(--slate-1); + --secondary-2: var(--slate-2); + --secondary-3: var(--slate-3); + --secondary-4: var(--slate-4); + --secondary-5: var(--slate-5); + --secondary-6: var(--slate-6); + --secondary-7: var(--slate-7); + --secondary-8: var(--slate-8); + --secondary-9: var(--slate-9); + --secondary-10: var(--slate-10); + --secondary-11: var(--slate-11); + --secondary-12: var(--slate-12); + --secondary-a1: var(--slate-a1); + --secondary-a2: var(--slate-a2); + --secondary-a3: var(--slate-a3); + --secondary-a4: var(--slate-a4); + --secondary-a5: var(--slate-a5); + --secondary-a6: var(--slate-a6); + --secondary-a7: var(--slate-a7); + --secondary-a8: var(--slate-a8); + --secondary-a9: var(--slate-a9); + --secondary-a10: var(--slate-a10); + --secondary-a11: var(--slate-a11); + --secondary-a12: var(--slate-a12); + /* Info */ + --info-1: var(--blue-1); + --info-2: var(--blue-2); + --info-3: var(--blue-3); + --info-4: var(--blue-4); + --info-5: var(--blue-5); + --info-6: var(--blue-6); + --info-7: var(--blue-7); + --info-8: var(--blue-8); + --info-9: var(--blue-9); + --info-10: var(--blue-10); + --info-11: var(--blue-11); + --info-12: var(--blue-12); + --info-a1: var(--blue-a1); + --info-a2: var(--blue-a2); + --info-a3: var(--blue-a3); + --info-a4: var(--blue-a4); + --info-a5: var(--blue-a5); + --info-a6: var(--blue-a6); + --info-a7: var(--blue-a7); + --info-a8: var(--blue-a8); + --info-a9: var(--blue-a9); + --info-a10: var(--blue-a10); + --info-a11: var(--blue-a11); + --info-a12: var(--blue-a12); + /* Success */ + --success-1: var(--jade-1); + --success-2: var(--jade-2); + --success-3: var(--jade-3); + --success-4: var(--jade-4); + --success-5: var(--jade-5); + --success-6: var(--jade-6); + --success-7: var(--jade-7); + --success-8: var(--jade-8); + --success-9: var(--jade-9); + --success-10: var(--jade-10); + --success-11: var(--jade-11); + --success-12: var(--jade-12); + --success-a1: var(--jade-a1); + --success-a2: var(--jade-a2); + --success-a3: var(--jade-a3); + --success-a4: var(--jade-a4); + --success-a5: var(--jade-a5); + --success-a6: var(--jade-a6); + --success-a7: var(--jade-a7); + --success-a8: var(--jade-a8); + --success-a9: var(--jade-a9); + --success-a10: var(--jade-a10); + --success-a11: var(--jade-a11); + --success-a12: var(--jade-a12); + /* Destructive */ + --destructive-1: var(--red-1); + --destructive-2: var(--red-2); + --destructive-3: var(--red-3); + --destructive-4: var(--red-4); + --destructive-5: var(--red-5); + --destructive-6: var(--red-6); + --destructive-7: var(--red-7); + --destructive-8: var(--red-8); + --destructive-9: var(--red-9); + --destructive-10: var(--red-10); + --destructive-11: var(--red-11); + --destructive-12: var(--red-12); + --destructive-a1: var(--red-a1); + --destructive-a2: var(--red-a2); + --destructive-a3: var(--red-a3); + --destructive-a4: var(--red-a4); + --destructive-a5: var(--red-a5); + --destructive-a6: var(--red-a6); + --destructive-a7: var(--red-a7); + --destructive-a8: var(--red-a8); + --destructive-a9: var(--red-a9); + --destructive-a10: var(--red-a10); + --destructive-a11: var(--red-a11); + --destructive-a12: var(--red-a12); + /* Warning */ + --warning-1: var(--amber-1); + --warning-2: var(--amber-2); + --warning-3: var(--amber-3); + --warning-4: var(--amber-4); + --warning-5: var(--amber-5); + --warning-6: var(--amber-6); + --warning-7: var(--amber-7); + --warning-8: var(--amber-8); + --warning-9: var(--amber-9); + --warning-10: var(--amber-10); + --warning-11: var(--amber-11); + --warning-12: var(--amber-12); + --warning-a1: var(--amber-a1); + --warning-a2: var(--amber-a2); + --warning-a3: var(--amber-a3); + --warning-a4: var(--amber-a4); + --warning-a5: var(--amber-a5); + --warning-a6: var(--amber-a6); + --warning-a7: var(--amber-a7); + --warning-a8: var(--amber-a8); + --warning-a9: var(--amber-a9); + --warning-a10: var(--amber-a10); + --warning-a11: var(--amber-a11); + --warning-a12: var(--amber-a12); + /* Radius */ + --radius: 0.5rem; + /* Font */ + --font-sans: "Instrument Sans", sans-serif; + --font-mono: "JetBrains Mono", monospace; + --font-instrument-sans: 'Instrument Sans', sans-serif; + --font-source-code-pro: 'Source Code Pro', monospace; + --font-jetbrains: "JetBrains Mono", monospace; + /* Shadow (light) */ + --shadow-small-adaptive: 0px 2px 5px 0px rgba(28, 32, 36, 0.03); + --shadow-medium-adaptive: 0px 4px 8px 0px rgba(28, 32, 36, 0.04); + --shadow-large-adaptive: + 0px 24px 12px 0px rgba(28, 32, 36, 0.02), + 0px 8px 8px 0px rgba(28, 32, 36, 0.02), + 0px 2px 6px 0px rgba(28, 32, 36, 0.02); + --shadow-large-negative-adaptive: + 0px -24px 12px 0px rgba(28, 32, 36, 0.02), + 0px -8px 8px 0px rgba(28, 32, 36, 0.02), + 0px -2px 6px 0px rgba(28, 32, 36, 0.02); + --shadow-large-negative-primary-adaptive: + 0px -32px 48px 0px color-mix(in srgb, var(--primary-9) 8%, transparent), + 0px -16px 32px 0px color-mix(in srgb, var(--primary-9) 10%, transparent), + 0px -8px 24px 0px color-mix(in srgb, var(--primary-9) 12%, transparent), + 0px -2px 16px 0px color-mix(in srgb, var(--primary-9) 14%, transparent); + --shadow-x-large-adaptive: + 0 0 0 1px rgba(0, 0, 0, 0.04), + 0 4px 8px 0 rgba(0, 0, 0, 0.02), + 0 1px 1px 0 rgba(0, 0, 0, 0.01), + 0 4px 8px 0 rgba(0, 0, 0, 0.03), + 0 0 0 1px #fff inset; + --shadow-inner-adaptive: 0 6px 16px 0 rgba(0, 0, 0, 0.04) inset; + --shadow-button-outline-adaptive: + 0 -1px 0 0 rgba(0, 0, 0, 0.08) inset, + 0 0 0 1px rgba(0, 0, 0, 0.08) inset, + 0 1px 2px 0 rgba(0, 0, 0, 0.02), + 0 1px 4px 0 rgba(0, 0, 0, 0.02); + --shadow-card-xs-no-left-adaptive: + 0 -1px 0 0 rgba(0, 0, 0, 0.16) inset, + 0 1px 0 0 rgba(0, 0, 0, 0.08) inset, + -1px 0 0 0 rgba(0, 0, 0, 0.08) inset, + 0 1px 2px 0 rgba(0, 0, 0, 0.02), + 0 1px 4px 0 rgba(0, 0, 0, 0.02); + --shadow-card-small-adaptive: + 0 0 0 1px rgba(0, 0, 0, 0.04), + 0 4px 8px 0 rgba(0, 0, 0, 0.04), + 0 1px 1px 0 rgba(0, 0, 0, 0.01), + 0 2px 4px 0 rgba(0, 0, 0, 0.03); + color-scheme: light dark; +} + +.dark { + /* Shadow (dark) */ + --shadow-small-adaptive: none; + --shadow-medium-adaptive: none; + --shadow-large-adaptive: none; + --shadow-large-negative-adaptive: none; + --shadow-large-negative-primary-adaptive: + 0px -32px 48px 0px color-mix(in srgb, var(--primary-9) 4%, transparent), + 0px -16px 32px 0px color-mix(in srgb, var(--primary-9) 8%, transparent), + 0px -8px 24px 0px color-mix(in srgb, var(--primary-9) 12%, transparent), + 0px -2px 16px 0px color-mix(in srgb, var(--primary-9) 16%, transparent); + --shadow-x-large-adaptive: none; + --shadow-inner-adaptive: none; + --shadow-button-outline-adaptive: none; + --shadow-card-xs-no-left-adaptive: none; + --shadow-card-small-adaptive: none; +} + +@theme { + /* Custom Palette */ + --color-white-1: var(--c-white-1); + --color-slate-1: var(--c-slate-1); + --color-slate-2: var(--c-slate-2); + --color-slate-3: var(--c-slate-3); + --color-slate-4: var(--c-slate-4); + --color-slate-5: var(--c-slate-5); + --color-slate-6: var(--c-slate-6); + --color-slate-7: var(--c-slate-7); + --color-slate-8: var(--c-slate-8); + --color-slate-9: var(--c-slate-9); + --color-slate-10: var(--c-slate-10); + --color-slate-11: var(--c-slate-11); + --color-slate-12: var(--c-slate-12); + --color-violet-2: var(--c-violet-2); + --color-violet-3: var(--c-violet-3); + --color-violet-4: var(--c-violet-4); + --color-violet-5: var(--c-violet-5); + --color-violet-6: var(--c-violet-6); + --color-violet-7: var(--c-violet-7); + --color-violet-8: var(--c-violet-8); + --color-violet-9: var(--c-violet-9); + --color-violet-10: var(--c-violet-10); + --color-violet-11: var(--c-violet-11); + --color-violet-12: var(--c-violet-12); + --color-transparent: transparent; + /* Contrast */ + --color-primary-contrast: var(--primary-contrast); + /* Primary */ + --color-primary-1: var(--primary-1); + --color-primary-2: var(--primary-2); + --color-primary-3: var(--primary-3); + --color-primary-4: var(--primary-4); + --color-primary-5: var(--primary-5); + --color-primary-6: var(--primary-6); + --color-primary-7: var(--primary-7); + --color-primary-8: var(--primary-8); + --color-primary-9: var(--primary-9); + --color-primary-10: var(--primary-10); + --color-primary-11: var(--primary-11); + --color-primary-12: var(--primary-12); + --color-primary-a1: var(--primary-a1); + --color-primary-a2: var(--primary-a2); + --color-primary-a3: var(--primary-a3); + --color-primary-a4: var(--primary-a4); + --color-primary-a5: var(--primary-a5); + --color-primary-a6: var(--primary-a6); + --color-primary-a7: var(--primary-a7); + --color-primary-a8: var(--primary-a8); + --color-primary-a9: var(--primary-a9); + --color-primary-a10: var(--primary-a10); + --color-primary-a11: var(--primary-a11); + --color-primary-a12: var(--primary-a12); + /* Secondary */ + --color-secondary-1: var(--secondary-1); + --color-secondary-2: var(--secondary-2); + --color-secondary-3: var(--secondary-3); + --color-secondary-4: var(--secondary-4); + --color-secondary-5: var(--secondary-5); + --color-secondary-6: var(--secondary-6); + --color-secondary-7: var(--secondary-7); + --color-secondary-8: var(--secondary-8); + --color-secondary-9: var(--secondary-9); + --color-secondary-10: var(--secondary-10); + --color-secondary-11: var(--secondary-11); + --color-secondary-12: var(--secondary-12); + --color-secondary-a1: var(--secondary-a1); + --color-secondary-a2: var(--secondary-a2); + --color-secondary-a3: var(--secondary-a3); + --color-secondary-a4: var(--secondary-a4); + --color-secondary-a5: var(--secondary-a5); + --color-secondary-a6: var(--secondary-a6); + --color-secondary-a7: var(--secondary-a7); + --color-secondary-a8: var(--secondary-a8); + --color-secondary-a9: var(--secondary-a9); + --color-secondary-a10: var(--secondary-a10); + --color-secondary-a11: var(--secondary-a11); + --color-secondary-a12: var(--secondary-a12); + /* Info */ + --color-info-1: var(--info-1); + --color-info-2: var(--info-2); + --color-info-3: var(--info-3); + --color-info-4: var(--info-4); + --color-info-5: var(--info-5); + --color-info-6: var(--info-6); + --color-info-7: var(--info-7); + --color-info-8: var(--info-8); + --color-info-9: var(--info-9); + --color-info-10: var(--info-10); + --color-info-11: var(--info-11); + --color-info-12: var(--info-12); + --color-info-a1: var(--info-a1); + --color-info-a2: var(--info-a2); + --color-info-a3: var(--info-a3); + --color-info-a4: var(--info-a4); + --color-info-a5: var(--info-a5); + --color-info-a6: var(--info-a6); + --color-info-a7: var(--info-a7); + --color-info-a8: var(--info-a8); + --color-info-a9: var(--info-a9); + --color-info-a10: var(--info-a10); + --color-info-a11: var(--info-a11); + --color-info-a12: var(--info-a12); + /* Success */ + --color-success-1: var(--success-1); + --color-success-2: var(--success-2); + --color-success-3: var(--success-3); + --color-success-4: var(--success-4); + --color-success-5: var(--success-5); + --color-success-6: var(--success-6); + --color-success-7: var(--success-7); + --color-success-8: var(--success-8); + --color-success-9: var(--success-9); + --color-success-10: var(--success-10); + --color-success-11: var(--success-11); + --color-success-12: var(--success-12); + --color-success-a1: var(--success-a1); + --color-success-a2: var(--success-a2); + --color-success-a3: var(--success-a3); + --color-success-a4: var(--success-a4); + --color-success-a5: var(--success-a5); + --color-success-a6: var(--success-a6); + --color-success-a7: var(--success-a7); + --color-success-a8: var(--success-a8); + --color-success-a9: var(--success-a9); + --color-success-a10: var(--success-a10); + --color-success-a11: var(--success-a11); + --color-success-a12: var(--success-a12); + /* Warning */ + --color-warning-1: var(--warning-1); + --color-warning-2: var(--warning-2); + --color-warning-3: var(--warning-3); + --color-warning-4: var(--warning-4); + --color-warning-5: var(--warning-5); + --color-warning-6: var(--warning-6); + --color-warning-7: var(--warning-7); + --color-warning-8: var(--warning-8); + --color-warning-9: var(--warning-9); + --color-warning-10: var(--warning-10); + --color-warning-11: var(--warning-11); + --color-warning-12: var(--warning-12); + --color-warning-a1: var(--warning-a1); + --color-warning-a2: var(--warning-a2); + --color-warning-a3: var(--warning-a3); + --color-warning-a4: var(--warning-a4); + --color-warning-a5: var(--warning-a5); + --color-warning-a6: var(--warning-a6); + --color-warning-a7: var(--warning-a7); + --color-warning-a8: var(--warning-a8); + --color-warning-a9: var(--warning-a9); + --color-warning-a10: var(--warning-a10); + --color-warning-a11: var(--warning-a11); + --color-warning-a12: var(--warning-a12); + /* Destructive */ + --color-destructive-1: var(--destructive-1); + --color-destructive-2: var(--destructive-2); + --color-destructive-3: var(--destructive-3); + --color-destructive-4: var(--destructive-4); + --color-destructive-5: var(--destructive-5); + --color-destructive-6: var(--destructive-6); + --color-destructive-7: var(--destructive-7); + --color-destructive-8: var(--destructive-8); + --color-destructive-9: var(--destructive-9); + --color-destructive-10: var(--destructive-10); + --color-destructive-11: var(--destructive-11); + --color-destructive-12: var(--destructive-12); + --color-destructive-a1: var(--destructive-a1); + --color-destructive-a2: var(--destructive-a2); + --color-destructive-a3: var(--destructive-a3); + --color-destructive-a4: var(--destructive-a4); + --color-destructive-a5: var(--destructive-a5); + --color-destructive-a6: var(--destructive-a6); + --color-destructive-a7: var(--destructive-a7); + --color-destructive-a8: var(--destructive-a8); + --color-destructive-a9: var(--destructive-a9); + --color-destructive-a10: var(--destructive-a10); + --color-destructive-a11: var(--destructive-a11); + --color-destructive-a12: var(--destructive-a12); + /* Radix Colors */ + --color-gray-1: var(--gray-1); + --color-gray-2: var(--gray-2); + --color-gray-3: var(--gray-3); + --color-gray-4: var(--gray-4); + --color-gray-5: var(--gray-5); + --color-gray-6: var(--gray-6); + --color-gray-7: var(--gray-7); + --color-gray-8: var(--gray-8); + --color-gray-9: var(--gray-9); + --color-gray-10: var(--gray-10); + --color-gray-11: var(--gray-11); + --color-gray-12: var(--gray-12); + --color-gray-a1: var(--gray-a1); + --color-gray-a2: var(--gray-a2); + --color-gray-a3: var(--gray-a3); + --color-gray-a4: var(--gray-a4); + --color-gray-a5: var(--gray-a5); + --color-gray-a6: var(--gray-a6); + --color-gray-a7: var(--gray-a7); + --color-gray-a8: var(--gray-a8); + --color-gray-a9: var(--gray-a9); + --color-gray-a10: var(--gray-a10); + --color-gray-a11: var(--gray-a11); + --color-gray-a12: var(--gray-a12); + + --color-mauve-1: var(--mauve-1); + --color-mauve-2: var(--mauve-2); + --color-mauve-3: var(--mauve-3); + --color-mauve-4: var(--mauve-4); + --color-mauve-5: var(--mauve-5); + --color-mauve-6: var(--mauve-6); + --color-mauve-7: var(--mauve-7); + --color-mauve-8: var(--mauve-8); + --color-mauve-9: var(--mauve-9); + --color-mauve-10: var(--mauve-10); + --color-mauve-11: var(--mauve-11); + --color-mauve-12: var(--mauve-12); + --color-mauve-a1: var(--mauve-a1); + --color-mauve-a2: var(--mauve-a2); + --color-mauve-a3: var(--mauve-a3); + --color-mauve-a4: var(--mauve-a4); + --color-mauve-a5: var(--mauve-a5); + --color-mauve-a6: var(--mauve-a6); + --color-mauve-a7: var(--mauve-a7); + --color-mauve-a8: var(--mauve-a8); + --color-mauve-a9: var(--mauve-a9); + --color-mauve-a10: var(--mauve-a10); + --color-mauve-a11: var(--mauve-a11); + --color-mauve-a12: var(--mauve-a12); + + --color-sage-1: var(--sage-1); + --color-sage-2: var(--sage-2); + --color-sage-3: var(--sage-3); + --color-sage-4: var(--sage-4); + --color-sage-5: var(--sage-5); + --color-sage-6: var(--sage-6); + --color-sage-7: var(--sage-7); + --color-sage-8: var(--sage-8); + --color-sage-9: var(--sage-9); + --color-sage-10: var(--sage-10); + --color-sage-11: var(--sage-11); + --color-sage-12: var(--sage-12); + --color-sage-a1: var(--sage-a1); + --color-sage-a2: var(--sage-a2); + --color-sage-a3: var(--sage-a3); + --color-sage-a4: var(--sage-a4); + --color-sage-a5: var(--sage-a5); + --color-sage-a6: var(--sage-a6); + --color-sage-a7: var(--sage-a7); + --color-sage-a8: var(--sage-a8); + --color-sage-a9: var(--sage-a9); + --color-sage-a10: var(--sage-a10); + --color-sage-a11: var(--sage-a11); + --color-sage-a12: var(--sage-a12); + + --color-olive-1: var(--olive-1); + --color-olive-2: var(--olive-2); + --color-olive-3: var(--olive-3); + --color-olive-4: var(--olive-4); + --color-olive-5: var(--olive-5); + --color-olive-6: var(--olive-6); + --color-olive-7: var(--olive-7); + --color-olive-8: var(--olive-8); + --color-olive-9: var(--olive-9); + --color-olive-10: var(--olive-10); + --color-olive-11: var(--olive-11); + --color-olive-12: var(--olive-12); + --color-olive-a1: var(--olive-a1); + --color-olive-a2: var(--olive-a2); + --color-olive-a3: var(--olive-a3); + --color-olive-a4: var(--olive-a4); + --color-olive-a5: var(--olive-a5); + --color-olive-a6: var(--olive-a6); + --color-olive-a7: var(--olive-a7); + --color-olive-a8: var(--olive-a8); + --color-olive-a9: var(--olive-a9); + --color-olive-a10: var(--olive-a10); + --color-olive-a11: var(--olive-a11); + --color-olive-a12: var(--olive-a12); + + --color-sand-1: var(--sand-1); + --color-sand-2: var(--sand-2); + --color-sand-3: var(--sand-3); + --color-sand-4: var(--sand-4); + --color-sand-5: var(--sand-5); + --color-sand-6: var(--sand-6); + --color-sand-7: var(--sand-7); + --color-sand-8: var(--sand-8); + --color-sand-9: var(--sand-9); + --color-sand-10: var(--sand-10); + --color-sand-11: var(--sand-11); + --color-sand-12: var(--sand-12); + --color-sand-a1: var(--sand-a1); + --color-sand-a2: var(--sand-a2); + --color-sand-a3: var(--sand-a3); + --color-sand-a4: var(--sand-a4); + --color-sand-a5: var(--sand-a5); + --color-sand-a6: var(--sand-a6); + --color-sand-a7: var(--sand-a7); + --color-sand-a8: var(--sand-a8); + --color-sand-a9: var(--sand-a9); + --color-sand-a10: var(--sand-a10); + --color-sand-a11: var(--sand-a11); + --color-sand-a12: var(--sand-a12); + + --color-tomato-1: var(--tomato-1); + --color-tomato-2: var(--tomato-2); + --color-tomato-3: var(--tomato-3); + --color-tomato-4: var(--tomato-4); + --color-tomato-5: var(--tomato-5); + --color-tomato-6: var(--tomato-6); + --color-tomato-7: var(--tomato-7); + --color-tomato-8: var(--tomato-8); + --color-tomato-9: var(--tomato-9); + --color-tomato-10: var(--tomato-10); + --color-tomato-11: var(--tomato-11); + --color-tomato-12: var(--tomato-12); + --color-tomato-a1: var(--tomato-a1); + --color-tomato-a2: var(--tomato-a2); + --color-tomato-a3: var(--tomato-a3); + --color-tomato-a4: var(--tomato-a4); + --color-tomato-a5: var(--tomato-a5); + --color-tomato-a6: var(--tomato-a6); + --color-tomato-a7: var(--tomato-a7); + --color-tomato-a8: var(--tomato-a8); + --color-tomato-a9: var(--tomato-a9); + --color-tomato-a10: var(--tomato-a10); + --color-tomato-a11: var(--tomato-a11); + --color-tomato-a12: var(--tomato-a12); + + --color-red-1: var(--red-1); + --color-red-2: var(--red-2); + --color-red-3: var(--red-3); + --color-red-4: var(--red-4); + --color-red-5: var(--red-5); + --color-red-6: var(--red-6); + --color-red-7: var(--red-7); + --color-red-8: var(--red-8); + --color-red-9: var(--red-9); + --color-red-10: var(--red-10); + --color-red-11: var(--red-11); + --color-red-12: var(--red-12); + --color-red-a1: var(--red-a1); + --color-red-a2: var(--red-a2); + --color-red-a3: var(--red-a3); + --color-red-a4: var(--red-a4); + --color-red-a5: var(--red-a5); + --color-red-a6: var(--red-a6); + --color-red-a7: var(--red-a7); + --color-red-a8: var(--red-a8); + --color-red-a9: var(--red-a9); + --color-red-a10: var(--red-a10); + --color-red-a11: var(--red-a11); + --color-red-a12: var(--red-a12); + + --color-ruby-1: var(--ruby-1); + --color-ruby-2: var(--ruby-2); + --color-ruby-3: var(--ruby-3); + --color-ruby-4: var(--ruby-4); + --color-ruby-5: var(--ruby-5); + --color-ruby-6: var(--ruby-6); + --color-ruby-7: var(--ruby-7); + --color-ruby-8: var(--ruby-8); + --color-ruby-9: var(--ruby-9); + --color-ruby-10: var(--ruby-10); + --color-ruby-11: var(--ruby-11); + --color-ruby-12: var(--ruby-12); + --color-ruby-a1: var(--ruby-a1); + --color-ruby-a2: var(--ruby-a2); + --color-ruby-a3: var(--ruby-a3); + --color-ruby-a4: var(--ruby-a4); + --color-ruby-a5: var(--ruby-a5); + --color-ruby-a6: var(--ruby-a6); + --color-ruby-a7: var(--ruby-a7); + --color-ruby-a8: var(--ruby-a8); + --color-ruby-a9: var(--ruby-a9); + --color-ruby-a10: var(--ruby-a10); + --color-ruby-a11: var(--ruby-a11); + --color-ruby-a12: var(--ruby-a12); + + --color-crimson-1: var(--crimson-1); + --color-crimson-2: var(--crimson-2); + --color-crimson-3: var(--crimson-3); + --color-crimson-4: var(--crimson-4); + --color-crimson-5: var(--crimson-5); + --color-crimson-6: var(--crimson-6); + --color-crimson-7: var(--crimson-7); + --color-crimson-8: var(--crimson-8); + --color-crimson-9: var(--crimson-9); + --color-crimson-10: var(--crimson-10); + --color-crimson-11: var(--crimson-11); + --color-crimson-12: var(--crimson-12); + --color-crimson-a1: var(--crimson-a1); + --color-crimson-a2: var(--crimson-a2); + --color-crimson-a3: var(--crimson-a3); + --color-crimson-a4: var(--crimson-a4); + --color-crimson-a5: var(--crimson-a5); + --color-crimson-a6: var(--crimson-a6); + --color-crimson-a7: var(--crimson-a7); + --color-crimson-a8: var(--crimson-a8); + --color-crimson-a9: var(--crimson-a9); + --color-crimson-a10: var(--crimson-a10); + --color-crimson-a11: var(--crimson-a11); + --color-crimson-a12: var(--crimson-a12); + + --color-pink-1: var(--pink-1); + --color-pink-2: var(--pink-2); + --color-pink-3: var(--pink-3); + --color-pink-4: var(--pink-4); + --color-pink-5: var(--pink-5); + --color-pink-6: var(--pink-6); + --color-pink-7: var(--pink-7); + --color-pink-8: var(--pink-8); + --color-pink-9: var(--pink-9); + --color-pink-10: var(--pink-10); + --color-pink-11: var(--pink-11); + --color-pink-12: var(--pink-12); + --color-pink-a1: var(--pink-a1); + --color-pink-a2: var(--pink-a2); + --color-pink-a3: var(--pink-a3); + --color-pink-a4: var(--pink-a4); + --color-pink-a5: var(--pink-a5); + --color-pink-a6: var(--pink-a6); + --color-pink-a7: var(--pink-a7); + --color-pink-a8: var(--pink-a8); + --color-pink-a9: var(--pink-a9); + --color-pink-a10: var(--pink-a10); + --color-pink-a11: var(--pink-a11); + --color-pink-a12: var(--pink-a12); + + --color-plum-1: var(--plum-1); + --color-plum-2: var(--plum-2); + --color-plum-3: var(--plum-3); + --color-plum-4: var(--plum-4); + --color-plum-5: var(--plum-5); + --color-plum-6: var(--plum-6); + --color-plum-7: var(--plum-7); + --color-plum-8: var(--plum-8); + --color-plum-9: var(--plum-9); + --color-plum-10: var(--plum-10); + --color-plum-11: var(--plum-11); + --color-plum-12: var(--plum-12); + --color-plum-a1: var(--plum-a1); + --color-plum-a2: var(--plum-a2); + --color-plum-a3: var(--plum-a3); + --color-plum-a4: var(--plum-a4); + --color-plum-a5: var(--plum-a5); + --color-plum-a6: var(--plum-a6); + --color-plum-a7: var(--plum-a7); + --color-plum-a8: var(--plum-a8); + --color-plum-a9: var(--plum-a9); + --color-plum-a10: var(--plum-a10); + --color-plum-a11: var(--plum-a11); + --color-plum-a12: var(--plum-a12); + + --color-purple-1: var(--purple-1); + --color-purple-2: var(--purple-2); + --color-purple-3: var(--purple-3); + --color-purple-4: var(--purple-4); + --color-purple-5: var(--purple-5); + --color-purple-6: var(--purple-6); + --color-purple-7: var(--purple-7); + --color-purple-8: var(--purple-8); + --color-purple-9: var(--purple-9); + --color-purple-10: var(--purple-10); + --color-purple-11: var(--purple-11); + --color-purple-12: var(--purple-12); + --color-purple-a1: var(--purple-a1); + --color-purple-a2: var(--purple-a2); + --color-purple-a3: var(--purple-a3); + --color-purple-a4: var(--purple-a4); + --color-purple-a5: var(--purple-a5); + --color-purple-a6: var(--purple-a6); + --color-purple-a7: var(--purple-a7); + --color-purple-a8: var(--purple-a8); + --color-purple-a9: var(--purple-a9); + --color-purple-a10: var(--purple-a10); + --color-purple-a11: var(--purple-a11); + --color-purple-a12: var(--purple-a12); + + --color-iris-1: var(--iris-1); + --color-iris-2: var(--iris-2); + --color-iris-3: var(--iris-3); + --color-iris-4: var(--iris-4); + --color-iris-5: var(--iris-5); + --color-iris-6: var(--iris-6); + --color-iris-7: var(--iris-7); + --color-iris-8: var(--iris-8); + --color-iris-9: var(--iris-9); + --color-iris-10: var(--iris-10); + --color-iris-11: var(--iris-11); + --color-iris-12: var(--iris-12); + --color-iris-a1: var(--iris-a1); + --color-iris-a2: var(--iris-a2); + --color-iris-a3: var(--iris-a3); + --color-iris-a4: var(--iris-a4); + --color-iris-a5: var(--iris-a5); + --color-iris-a6: var(--iris-a6); + --color-iris-a7: var(--iris-a7); + --color-iris-a8: var(--iris-a8); + --color-iris-a9: var(--iris-a9); + --color-iris-a10: var(--iris-a10); + --color-iris-a11: var(--iris-a11); + --color-iris-a12: var(--iris-a12); + + --color-indigo-1: var(--indigo-1); + --color-indigo-2: var(--indigo-2); + --color-indigo-3: var(--indigo-3); + --color-indigo-4: var(--indigo-4); + --color-indigo-5: var(--indigo-5); + --color-indigo-6: var(--indigo-6); + --color-indigo-7: var(--indigo-7); + --color-indigo-8: var(--indigo-8); + --color-indigo-9: var(--indigo-9); + --color-indigo-10: var(--indigo-10); + --color-indigo-11: var(--indigo-11); + --color-indigo-12: var(--indigo-12); + --color-indigo-a1: var(--indigo-a1); + --color-indigo-a2: var(--indigo-a2); + --color-indigo-a3: var(--indigo-a3); + --color-indigo-a4: var(--indigo-a4); + --color-indigo-a5: var(--indigo-a5); + --color-indigo-a6: var(--indigo-a6); + --color-indigo-a7: var(--indigo-a7); + --color-indigo-a8: var(--indigo-a8); + --color-indigo-a9: var(--indigo-a9); + --color-indigo-a10: var(--indigo-a10); + --color-indigo-a11: var(--indigo-a11); + --color-indigo-a12: var(--indigo-a12); + + --color-blue-1: var(--blue-1); + --color-blue-2: var(--blue-2); + --color-blue-3: var(--blue-3); + --color-blue-4: var(--blue-4); + --color-blue-5: var(--blue-5); + --color-blue-6: var(--blue-6); + --color-blue-7: var(--blue-7); + --color-blue-8: var(--blue-8); + --color-blue-9: var(--blue-9); + --color-blue-10: var(--blue-10); + --color-blue-11: var(--blue-11); + --color-blue-12: var(--blue-12); + --color-blue-a1: var(--blue-a1); + --color-blue-a2: var(--blue-a2); + --color-blue-a3: var(--blue-a3); + --color-blue-a4: var(--blue-a4); + --color-blue-a5: var(--blue-a5); + --color-blue-a6: var(--blue-a6); + --color-blue-a7: var(--blue-a7); + --color-blue-a8: var(--blue-a8); + --color-blue-a9: var(--blue-a9); + --color-blue-a10: var(--blue-a10); + --color-blue-a11: var(--blue-a11); + --color-blue-a12: var(--blue-a12); + + --color-cyan-1: var(--cyan-1); + --color-cyan-2: var(--cyan-2); + --color-cyan-3: var(--cyan-3); + --color-cyan-4: var(--cyan-4); + --color-cyan-5: var(--cyan-5); + --color-cyan-6: var(--cyan-6); + --color-cyan-7: var(--cyan-7); + --color-cyan-8: var(--cyan-8); + --color-cyan-9: var(--cyan-9); + --color-cyan-10: var(--cyan-10); + --color-cyan-11: var(--cyan-11); + --color-cyan-12: var(--cyan-12); + --color-cyan-a1: var(--cyan-a1); + --color-cyan-a2: var(--cyan-a2); + --color-cyan-a3: var(--cyan-a3); + --color-cyan-a4: var(--cyan-a4); + --color-cyan-a5: var(--cyan-a5); + --color-cyan-a6: var(--cyan-a6); + --color-cyan-a7: var(--cyan-a7); + --color-cyan-a8: var(--cyan-a8); + --color-cyan-a9: var(--cyan-a9); + --color-cyan-a10: var(--cyan-a10); + --color-cyan-a11: var(--cyan-a11); + --color-cyan-a12: var(--cyan-a12); + + --color-teal-1: var(--teal-1); + --color-teal-2: var(--teal-2); + --color-teal-3: var(--teal-3); + --color-teal-4: var(--teal-4); + --color-teal-5: var(--teal-5); + --color-teal-6: var(--teal-6); + --color-teal-7: var(--teal-7); + --color-teal-8: var(--teal-8); + --color-teal-9: var(--teal-9); + --color-teal-10: var(--teal-10); + --color-teal-11: var(--teal-11); + --color-teal-12: var(--teal-12); + --color-teal-a1: var(--teal-a1); + --color-teal-a2: var(--teal-a2); + --color-teal-a3: var(--teal-a3); + --color-teal-a4: var(--teal-a4); + --color-teal-a5: var(--teal-a5); + --color-teal-a6: var(--teal-a6); + --color-teal-a7: var(--teal-a7); + --color-teal-a8: var(--teal-a8); + --color-teal-a9: var(--teal-a9); + --color-teal-a10: var(--teal-a10); + --color-teal-a11: var(--teal-a11); + --color-teal-a12: var(--teal-a12); + + --color-jade-1: var(--jade-1); + --color-jade-2: var(--jade-2); + --color-jade-3: var(--jade-3); + --color-jade-4: var(--jade-4); + --color-jade-5: var(--jade-5); + --color-jade-6: var(--jade-6); + --color-jade-7: var(--jade-7); + --color-jade-8: var(--jade-8); + --color-jade-9: var(--jade-9); + --color-jade-10: var(--jade-10); + --color-jade-11: var(--jade-11); + --color-jade-12: var(--jade-12); + --color-jade-a1: var(--jade-a1); + --color-jade-a2: var(--jade-a2); + --color-jade-a3: var(--jade-a3); + --color-jade-a4: var(--jade-a4); + --color-jade-a5: var(--jade-a5); + --color-jade-a6: var(--jade-a6); + --color-jade-a7: var(--jade-a7); + --color-jade-a8: var(--jade-a8); + --color-jade-a9: var(--jade-a9); + --color-jade-a10: var(--jade-a10); + --color-jade-a11: var(--jade-a11); + --color-jade-a12: var(--jade-a12); + + --color-green-1: var(--green-1); + --color-green-2: var(--green-2); + --color-green-3: var(--green-3); + --color-green-4: var(--green-4); + --color-green-5: var(--green-5); + --color-green-6: var(--green-6); + --color-green-7: var(--green-7); + --color-green-8: var(--green-8); + --color-green-9: var(--green-9); + --color-green-10: var(--green-10); + --color-green-11: var(--green-11); + --color-green-12: var(--green-12); + --color-green-a1: var(--green-a1); + --color-green-a2: var(--green-a2); + --color-green-a3: var(--green-a3); + --color-green-a4: var(--green-a4); + --color-green-a5: var(--green-a5); + --color-green-a6: var(--green-a6); + --color-green-a7: var(--green-a7); + --color-green-a8: var(--green-a8); + --color-green-a9: var(--green-a9); + --color-green-a10: var(--green-a10); + --color-green-a11: var(--green-a11); + --color-green-a12: var(--green-a12); + + --color-grass-1: var(--grass-1); + --color-grass-2: var(--grass-2); + --color-grass-3: var(--grass-3); + --color-grass-4: var(--grass-4); + --color-grass-5: var(--grass-5); + --color-grass-6: var(--grass-6); + --color-grass-7: var(--grass-7); + --color-grass-8: var(--grass-8); + --color-grass-9: var(--grass-9); + --color-grass-10: var(--grass-10); + --color-grass-11: var(--grass-11); + --color-grass-12: var(--grass-12); + --color-grass-a1: var(--grass-a1); + --color-grass-a2: var(--grass-a2); + --color-grass-a3: var(--grass-a3); + --color-grass-a4: var(--grass-a4); + --color-grass-a5: var(--grass-a5); + --color-grass-a6: var(--grass-a6); + --color-grass-a7: var(--grass-a7); + --color-grass-a8: var(--grass-a8); + --color-grass-a9: var(--grass-a9); + --color-grass-a10: var(--grass-a10); + --color-grass-a11: var(--grass-a11); + --color-grass-a12: var(--grass-a12); + + --color-brown-1: var(--brown-1); + --color-brown-2: var(--brown-2); + --color-brown-3: var(--brown-3); + --color-brown-4: var(--brown-4); + --color-brown-5: var(--brown-5); + --color-brown-6: var(--brown-6); + --color-brown-7: var(--brown-7); + --color-brown-8: var(--brown-8); + --color-brown-9: var(--brown-9); + --color-brown-10: var(--brown-10); + --color-brown-11: var(--brown-11); + --color-brown-12: var(--brown-12); + --color-brown-a1: var(--brown-a1); + --color-brown-a2: var(--brown-a2); + --color-brown-a3: var(--brown-a3); + --color-brown-a4: var(--brown-a4); + --color-brown-a5: var(--brown-a5); + --color-brown-a6: var(--brown-a6); + --color-brown-a7: var(--brown-a7); + --color-brown-a8: var(--brown-a8); + --color-brown-a9: var(--brown-a9); + --color-brown-a10: var(--brown-a10); + --color-brown-a11: var(--brown-a11); + --color-brown-a12: var(--brown-a12); + + --color-orange-1: var(--orange-1); + --color-orange-2: var(--orange-2); + --color-orange-3: var(--orange-3); + --color-orange-4: var(--orange-4); + --color-orange-5: var(--orange-5); + --color-orange-6: var(--orange-6); + --color-orange-7: var(--orange-7); + --color-orange-8: var(--orange-8); + --color-orange-9: var(--orange-9); + --color-orange-10: var(--orange-10); + --color-orange-11: var(--orange-11); + --color-orange-12: var(--orange-12); + --color-orange-a1: var(--orange-a1); + --color-orange-a2: var(--orange-a2); + --color-orange-a3: var(--orange-a3); + --color-orange-a4: var(--orange-a4); + --color-orange-a5: var(--orange-a5); + --color-orange-a6: var(--orange-a6); + --color-orange-a7: var(--orange-a7); + --color-orange-a8: var(--orange-a8); + --color-orange-a9: var(--orange-a9); + --color-orange-a10: var(--orange-a10); + --color-orange-a11: var(--orange-a11); + --color-orange-a12: var(--orange-a12); + + --color-sky-1: var(--sky-1); + --color-sky-2: var(--sky-2); + --color-sky-3: var(--sky-3); + --color-sky-4: var(--sky-4); + --color-sky-5: var(--sky-5); + --color-sky-6: var(--sky-6); + --color-sky-7: var(--sky-7); + --color-sky-8: var(--sky-8); + --color-sky-9: var(--sky-9); + --color-sky-10: var(--sky-10); + --color-sky-11: var(--sky-11); + --color-sky-12: var(--sky-12); + --color-sky-a1: var(--sky-a1); + --color-sky-a2: var(--sky-a2); + --color-sky-a3: var(--sky-a3); + --color-sky-a4: var(--sky-a4); + --color-sky-a5: var(--sky-a5); + --color-sky-a6: var(--sky-a6); + --color-sky-a7: var(--sky-a7); + --color-sky-a8: var(--sky-a8); + --color-sky-a9: var(--sky-a9); + --color-sky-a10: var(--sky-a10); + --color-sky-a11: var(--sky-a11); + --color-sky-a12: var(--sky-a12); + + --color-mint-1: var(--mint-1); + --color-mint-2: var(--mint-2); + --color-mint-3: var(--mint-3); + --color-mint-4: var(--mint-4); + --color-mint-5: var(--mint-5); + --color-mint-6: var(--mint-6); + --color-mint-7: var(--mint-7); + --color-mint-8: var(--mint-8); + --color-mint-9: var(--mint-9); + --color-mint-10: var(--mint-10); + --color-mint-11: var(--mint-11); + --color-mint-12: var(--mint-12); + --color-mint-a1: var(--mint-a1); + --color-mint-a2: var(--mint-a2); + --color-mint-a3: var(--mint-a3); + --color-mint-a4: var(--mint-a4); + --color-mint-a5: var(--mint-a5); + --color-mint-a6: var(--mint-a6); + --color-mint-a7: var(--mint-a7); + --color-mint-a8: var(--mint-a8); + --color-mint-a9: var(--mint-a9); + --color-mint-a10: var(--mint-a10); + --color-mint-a11: var(--mint-a11); + --color-mint-a12: var(--mint-a12); + + --color-lime-1: var(--lime-1); + --color-lime-2: var(--lime-2); + --color-lime-3: var(--lime-3); + --color-lime-4: var(--lime-4); + --color-lime-5: var(--lime-5); + --color-lime-6: var(--lime-6); + --color-lime-7: var(--lime-7); + --color-lime-8: var(--lime-8); + --color-lime-9: var(--lime-9); + --color-lime-10: var(--lime-10); + --color-lime-11: var(--lime-11); + --color-lime-12: var(--lime-12); + --color-lime-a1: var(--lime-a1); + --color-lime-a2: var(--lime-a2); + --color-lime-a3: var(--lime-a3); + --color-lime-a4: var(--lime-a4); + --color-lime-a5: var(--lime-a5); + --color-lime-a6: var(--lime-a6); + --color-lime-a7: var(--lime-a7); + --color-lime-a8: var(--lime-a8); + --color-lime-a9: var(--lime-a9); + --color-lime-a10: var(--lime-a10); + --color-lime-a11: var(--lime-a11); + --color-lime-a12: var(--lime-a12); + + --color-yellow-1: var(--yellow-1); + --color-yellow-2: var(--yellow-2); + --color-yellow-3: var(--yellow-3); + --color-yellow-4: var(--yellow-4); + --color-yellow-5: var(--yellow-5); + --color-yellow-6: var(--yellow-6); + --color-yellow-7: var(--yellow-7); + --color-yellow-8: var(--yellow-8); + --color-yellow-9: var(--yellow-9); + --color-yellow-10: var(--yellow-10); + --color-yellow-11: var(--yellow-11); + --color-yellow-12: var(--yellow-12); + --color-yellow-a1: var(--yellow-a1); + --color-yellow-a2: var(--yellow-a2); + --color-yellow-a3: var(--yellow-a3); + --color-yellow-a4: var(--yellow-a4); + --color-yellow-a5: var(--yellow-a5); + --color-yellow-a6: var(--yellow-a6); + --color-yellow-a7: var(--yellow-a7); + --color-yellow-a8: var(--yellow-a8); + --color-yellow-a9: var(--yellow-a9); + --color-yellow-a10: var(--yellow-a10); + --color-yellow-a11: var(--yellow-a11); + --color-yellow-a12: var(--yellow-a12); + + --color-amber-1: var(--amber-1); + --color-amber-2: var(--amber-2); + --color-amber-3: var(--amber-3); + --color-amber-4: var(--amber-4); + --color-amber-5: var(--amber-5); + --color-amber-6: var(--amber-6); + --color-amber-7: var(--amber-7); + --color-amber-8: var(--amber-8); + --color-amber-9: var(--amber-9); + --color-amber-10: var(--amber-10); + --color-amber-11: var(--amber-11); + --color-amber-12: var(--amber-12); + --color-amber-a1: var(--amber-a1); + --color-amber-a2: var(--amber-a2); + --color-amber-a3: var(--amber-a3); + --color-amber-a4: var(--amber-a4); + --color-amber-a5: var(--amber-a5); + --color-amber-a6: var(--amber-a6); + --color-amber-a7: var(--amber-a7); + --color-amber-a8: var(--amber-a8); + --color-amber-a9: var(--amber-a9); + --color-amber-a10: var(--amber-a10); + --color-amber-a11: var(--amber-a11); + --color-amber-a12: var(--amber-a12); + + --color-gold-1: var(--gold-1); + --color-gold-2: var(--gold-2); + --color-gold-3: var(--gold-3); + --color-gold-4: var(--gold-4); + --color-gold-5: var(--gold-5); + --color-gold-6: var(--gold-6); + --color-gold-7: var(--gold-7); + --color-gold-8: var(--gold-8); + --color-gold-9: var(--gold-9); + --color-gold-10: var(--gold-10); + --color-gold-11: var(--gold-11); + --color-gold-12: var(--gold-12); + --color-gold-a1: var(--gold-a1); + --color-gold-a2: var(--gold-a2); + --color-gold-a3: var(--gold-a3); + --color-gold-a4: var(--gold-a4); + --color-gold-a5: var(--gold-a5); + --color-gold-a6: var(--gold-a6); + --color-gold-a7: var(--gold-a7); + --color-gold-a8: var(--gold-a8); + --color-gold-a9: var(--gold-a9); + --color-gold-a10: var(--gold-a10); + --color-gold-a11: var(--gold-a11); + --color-gold-a12: var(--gold-a12); + + --color-bronze-1: var(--bronze-1); + --color-bronze-2: var(--bronze-2); + --color-bronze-3: var(--bronze-3); + --color-bronze-4: var(--bronze-4); + --color-bronze-5: var(--bronze-5); + --color-bronze-6: var(--bronze-6); + --color-bronze-7: var(--bronze-7); + --color-bronze-8: var(--bronze-8); + --color-bronze-9: var(--bronze-9); + --color-bronze-10: var(--bronze-10); + --color-bronze-11: var(--bronze-11); + --color-bronze-12: var(--bronze-12); + --color-bronze-a1: var(--bronze-a1); + --color-bronze-a2: var(--bronze-a2); + --color-bronze-a3: var(--bronze-a3); + --color-bronze-a4: var(--bronze-a4); + --color-bronze-a5: var(--bronze-a5); + --color-bronze-a6: var(--bronze-a6); + --color-bronze-a7: var(--bronze-a7); + --color-bronze-a8: var(--bronze-a8); + --color-bronze-a9: var(--bronze-a9); + --color-bronze-a10: var(--bronze-a10); + --color-bronze-a11: var(--bronze-a11); + --color-bronze-a12: var(--bronze-a12); + /* Marketing Colors */ + --color-m-slate-1: var(--m-slate-1); + --color-m-slate-2: var(--m-slate-2); + --color-m-slate-3: var(--m-slate-3); + --color-m-slate-4: var(--m-slate-4); + --color-m-slate-5: var(--m-slate-5); + --color-m-slate-6: var(--m-slate-6); + --color-m-slate-7: var(--m-slate-7); + --color-m-slate-8: var(--m-slate-8); + --color-m-slate-9: var(--m-slate-9); + --color-m-slate-10: var(--m-slate-10); + --color-m-slate-11: var(--m-slate-11); + --color-m-slate-12: var(--m-slate-12); + --color-m-slate-13: var(--m-slate-13); + --color-m-slate-14: var(--m-slate-14); + --color-m-slate-15: var(--m-slate-15); + --color-m-violet-1: var(--m-violet-1); + --color-m-violet-2: var(--m-violet-2); + --color-m-violet-3: var(--m-violet-3); + --color-m-violet-4: var(--m-violet-4); + --color-m-violet-5: var(--m-violet-5); + --color-m-violet-6: var(--m-violet-6); + --color-m-violet-7: var(--m-violet-7); + --color-m-violet-8: var(--m-violet-8); + --color-m-violet-9: var(--m-violet-9); + --color-m-violet-10: var(--m-violet-10); + --color-m-violet-11: var(--m-violet-11); + --color-m-violet-12: var(--m-violet-12); + + /* Font */ + --font-sans: var(--font-instrument-sans); + --font-mono: var(--font-jetbrains); + /* Shadow */ + --shadow-none: none; + --shadow-small: var(--shadow-small-adaptive); + --shadow-medium: var(--shadow-medium-adaptive); + --shadow-large: var(--shadow-large-adaptive); + --shadow-large-negative: var(--shadow-large-negative-adaptive); + --shadow-large-negative-primary: var(--shadow-large-negative-primary-adaptive); + --shadow-x-large: var(--shadow-x-large-adaptive); + --shadow-inner: var(--shadow-inner-adaptive); + --shadow-card: 0 0 0 1px rgba(0, 0, 0, 0.04), + 0 4px 8px 0 rgba(0, 0, 0, 0.07), + 0 1px 1px 0 rgba(0, 0, 0, 0.01), + 0 0 0 1px #FFF inset; + --shadow-button-bordered: + 0 0 0 1px var(--primary-9) inset, + 0 2px 0 0 rgba(255, 255, 255, 0.22) inset; + --shadow-button-outline: var(--shadow-button-outline-adaptive); + --shadow-card-xs-no-left: var(--shadow-card-xs-no-left-adaptive); + --shadow-card-small: var(--shadow-card-small-adaptive); + --shadow-card-dark: 0 0 0 1px var(--m-slate-9, #2A3037); + --text-xs: 0.8125rem; + --text-xs--line-height: 1.25rem; + --text-sm: 0.875rem; + --text-sm--line-height: 1.5rem; + --text-base: 1rem; + --text-base--line-height: 1.625rem; + --text-base--letter-spacing: -0.0025rem; + --text-lg: 1.125rem; + --text-lg--line-height: 1.625rem; + --text-xl: 1.25rem; + --text-xl--line-height: 1.75rem; + --text-2xl: 1.5rem; + --text-2xl--line-height: 2.25rem; + --text-2xl--letter-spacing: -0.0225rem; + --text-3xl: 2rem; + --text-3xl--line-height: 2.5rem; + --text-4xl: 2.5rem; + --text-4xl--line-height: 3rem; + --text-4xl--letter-spacing: -0.075rem; + --text-5xl: 3rem; + --text-5xl--line-height: 3.5rem; + --text-5xl--letter-spacing: -0.0975rem; + --text-6xl: 3.5rem; + --text-6xl--line-height: 4rem; + --text-6xl--letter-spacing: -0.1925rem; + --breakpoint-3xl: 110rem; + + /* Animation */ + --animate-accordion-down: accordion-down 0.2s ease-out; + --animate-accordion-up: accordion-up 0.2s ease-out; + --animate-spin: spin 1s linear infinite; + --animate-blur-in: blur-in 0.15s ease forwards; + --animate-border: border 3s linear infinite; + --animate-slide-in-right: slide-in-right both; + --animate-slide-in-left: slide-in-left both; + --animate-slide-in-up: slide-in-up both; + --animate-slide-in-down: slide-in-down both; + --animate-scale-rotate-in: scale-rotate-in both; + --animate-scale-in-top-right: scale-in-top-right both; + --animate-slide-down: slide-down 2.4s linear infinite; + --animate-slide-down-full: slide-down-full 2.4s linear infinite; + --animate-blink: blink 1.25s step-end infinite; + --animate-ellipse-1: ellipse-1 2400ms ease-out infinite; + --animate-ellipse-2: ellipse-2 2400ms ease-out infinite; + --animate-ellipse-3: ellipse-3 2400ms ease-out infinite; + --animate-ellipse-4: ellipse-4 2400ms ease-out infinite; + --animate-ellipse-reversed: ellipse-reversed 2400ms ease-out infinite; + /* Radius */ + --radius-ui-xxs: calc(var(--radius) - 0.25rem); + --radius-ui-xs: calc(var(--radius) - 0.125rem); + --radius-ui-sm: var(--radius); + --radius-ui-md: calc(var(--radius) + 0.125rem); + --radius-ui-lg: calc(var(--radius) + 0.25rem); + --radius-ui-xl: calc(var(--radius) + 0.375rem); + --radius-ui-2xl: calc(var(--radius) + 0.5rem); + /* Width */ + --layout-max-width: 81rem; + --docs-layout-max-width: 69rem; + + @keyframes accordion-down { + from { + height: 0; + } + + to { + height: var(--radix-accordion-content-height); + } + } + + @keyframes accordion-up { + from { + height: var(--radix-accordion-content-height); + } + + to { + height: 0; + } + } + + @keyframes spin { + from { + transform: rotate(0deg); + } + + to { + transform: rotate(360deg); + } + } + + @keyframes blur-in { + 0% { + filter: blur(4px); + } + + 100% { + filter: blur(0); + } + } + + @keyframes border { + to { + --border-angle: 360deg; + } + } + + @keyframes fade-in-scale { + 0% { + opacity: 0; + transform: scale(0.95); + } + + 100% { + opacity: 1; + transform: scale(1); + } + } + + @keyframes slide-in-right { + from { + transform: translateX(100%); + } + + to { + transform: translateX(0); + } + } + + @keyframes slide-in-left { + from { + transform: translateX(-100%); + } + + to { + transform: translateX(0); + } + } + + @keyframes slide-in-up { + from { + transform: translateY(100%); + } + + to { + transform: translateY(0); + } + } + + @keyframes slide-in-down { + from { + transform: translateY(-100%); + } + + to { + transform: translateY(0); + } + } + + @keyframes scale-rotate-in { + 0% { + transform: scale(0) rotate(-100deg); + } + + 100% { + transform: scale(1) rotate(0deg); + } + } + + @keyframes scale-in-top-right { + from { + transform: scale(0); + transform-origin: top right; + } + + to { + transform: scale(1); + transform-origin: top right; + } + } + + @keyframes slide-down-full { + 0% { + transform: translateY(-100%); + } + + 100% { + transform: translateY(calc(100vh + 5rem)); + } + + } + + @keyframes blink { + 50% { + opacity: 0; + } + } + + @keyframes slide-center-to-left { + + 0%, + 100% { + transform: translateX(0) translateY(-50%); + } + + 50% { + transform: translateX(-200%) translateY(-50%); + } + } + + @keyframes slide-center-to-right { + + 0%, + 100% { + transform: translateX(0) translateY(-50%); + } + + 50% { + transform: translateX(200%) translateY(-50%); + } + } + + @keyframes fade-scale-out { + 0% { + opacity: 1; + transform: scale(1); + } + + 7.5%, + 50% { + opacity: 0; + transform: scale(0.55); + } + + 56.5%, + 100% { + opacity: 1; + transform: scale(1); + } + } + + @keyframes fade-scale-in { + 0% { + opacity: 0; + transform: scale(0.55); + } + + 7.5%, + 50% { + opacity: 1; + transform: scale(1); + } + + 56.5%, + 100% { + opacity: 0; + transform: scale(0.55); + } + } + + @keyframes ellipse-slide-left { + 0% { + left: -13.75rem; + } + + 7.5% { + left: 17rem; + } + + 20% { + left: 17rem; + } + + 22% { + left: 14rem; + } + + 24% { + left: 10rem; + } + + 27.5% { + left: 4rem; + } + + 30% { + left: -2rem; + } + + 34.5% { + left: -13.75rem; + } + + 50% { + left: -13.75rem; + } + + 57.5% { + left: 17rem; + } + + 70% { + left: 17rem; + } + + 72% { + left: 14rem; + } + + 74% { + left: 10rem; + } + + 77.5% { + left: 4rem; + } + + 80% { + left: -2rem; + } + + 84.5% { + left: -13.75rem; + } + + 100% { + left: -13.75rem; + } + } + + @keyframes ellipse-slide-right { + 0% { + right: -13.75rem; + } + + 7.5% { + right: 17rem; + } + + 20% { + right: 17rem; + } + + 22% { + right: 14rem; + } + + 24% { + right: 10rem; + } + + 27.5% { + right: 4rem; + } + + 30% { + right: -2rem; + } + + 34.5% { + right: -13.75rem; + } + + 50% { + right: -13.75rem; + } + + 57.5% { + right: 17rem; + } + + 70% { + right: 17rem; + } + + 72% { + right: 14rem; + } + + 74% { + right: 10rem; + } + + 77.5% { + right: 4rem; + } + + 80% { + right: -2rem; + } + + 84.5% { + right: -13.75rem; + } + + 100% { + right: -13.75rem; + } + } + + @keyframes prompt-box-line { + 0% { + filter: blur(8px); + opacity: 0; + transform: scale(0.25); + } + + 100% { + filter: blur(0); + opacity: 1; + transform: scale(1); + } + } + + @keyframes ellipse-1 { + 0% { + top: calc(-100% - 16.81rem); + } + + 100% { + top: calc(100% + 2rem); + } + } + + @keyframes ellipse-2 { + 0% { + top: calc(-100% - 10.12rem); + } + + 100% { + top: calc(100% + 3.06rem); + } + } + + @keyframes ellipse-3 { + 0% { + top: calc(-100% - 10rem); + } + + 100% { + top: calc(100% + 11.69rem); + } + } + + @keyframes ellipse-4 { + 0% { + top: calc(-100% - 2rem); + } + + 100% { + top: calc(100% + 19.31rem); + } + } + + +} + +@layer base { + + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--c-slate-3, currentColor); + } + + body { + @apply isolate font-sans antialiased; + font-feature-settings: "rlig" 1, "calt" 1; + } + + :where([data-sonner-toast]) { + @apply font-sans text-sm; + } + + .section-content { + @apply flex flex-col justify-center items-center gap-10 mx-auto mt-4 mb-20 px-4 lg:px-6 pt-24 lg:pt-52 w-full max-w-6xl; + } + + .section-header { + @apply flex flex-col justify-center items-start lg:items-center gap-6 w-full lg:text-center text-start; + } + + .gradient-heading { + @apply inline-block bg-clip-text bg-gradient-to-r from-slate-12 to-slate-11 w-full text-transparent text-center text-balance; + } + + .table-header { + @apply justify-start pl-4 w-auto font-bold text-slate-12 text-sm; + } +} + +@layer utilities { + .font-instrument-sans { + font-family: var(--font-instrument-sans); + } + + .font-code { + font-family: var(--font-jetbrains); + font-size: 0.95rem; + font-style: normal; + font-weight: 500; + line-height: 1.5rem; + letter-spacing: -0.00406rem; + } + + .code-style { + font-family: var(--font-jetbrains); + font-size: 0.835rem; + font-style: normal; + font-weight: 400; + line-height: normal; + letter-spacing: -0.00406rem; + color: var(--c-slate-11); + background-color: var(--c-slate-3); + border-radius: 0.25rem; + border-width: 1px; + border-color: var(--c-slate-4); + /* padding: 0rem 0.125rem 0rem 0.125rem; */ + } + + .code-error-style { + font-family: var(--font-jetbrains); + font-size: 0.835rem; + font-style: normal; + font-weight: 500; + line-height: normal; + letter-spacing: -0.00406rem; + color: var(--red-11); + background-color: var(--red-3); + border-radius: 0.25rem; + border-width: 1px; + border-color: var(--red-5); + display: flex; + justify-content: center; + align-items: center; + } + + .markdown-code>p { + margin: 0 !important; + } + + .markdown-code>p>code { + font-family: var(--font-jetbrains); + font-size: 0.835rem; + font-style: normal; + font-weight: 400; + line-height: 1.75rem; + letter-spacing: -0.00406rem; + color: var(--c-violet-9); + background-color: var(--c-violet-3); + border-radius: 0.25rem; + border-width: 1px; + border-color: var(--c-violet-4); + padding: 0rem 0.125rem 0rem 0.125rem; + } + + .markdown-code code { + font-family: var(--font-jetbrains); + font-size: 0.835rem; + font-style: normal; + font-weight: 400; + line-height: 1.75rem; + letter-spacing: -0.00406rem; + color: var(--c-violet-9); + background-color: var(--c-violet-3); + border-radius: 0.25rem; + border-width: 1px; + border-color: var(--c-violet-4); + padding: 0rem 0.125rem 0rem 0.125rem; + } + + .code-block { + width: 100% !important; + height: auto !important; + margin: 0 !important; + padding: 1.75rem !important; + /* max-height: 350px !important; */ + border-radius: 0.75rem !important; + border: 1px solid var(--c-slate-4); + background: var(--c-slate-2); + color: var(--c-slate-12) !important; + resize: none !important; + outline: none !important; + scrollbar-width: thin !important; + font-family: var(--font-jetbrains) !important; + } + + .code-block>* { + background: transparent !important; + font-family: var(--font-jetbrains) !important; + font-size: 0.875rem !important; + font-style: normal !important; + font-weight: 400 !important; + line-height: 1.5rem !important; + /* 184.615% */ + letter-spacing: -0.00406rem !important; + } + + .code-block pre { + font-family: var(--font-jetbrains) !important; + padding: 0 !important; + font-size: 0.875rem !important; + font-weight: 400 !important; + line-height: 1.5rem !important; + color: var(--c-slate-12) !important; + background-color: transparent !important; + } + + .code-block code { + font-family: var(--font-jetbrains) !important; + padding: 0 !important; + font-size: 0.875rem !important; + font-weight: 400 !important; + line-height: 1.5rem !important; + color: var(--c-slate-12) !important; + background-color: transparent !important; + } + + @media (max-width: 768px) { + .code-block { + padding: 1rem !important; + } + } + + + .tab-style { + color: var(--c-slate-9); + cursor: pointer; + padding: 0.25em 0.5em; + font-size: 0.9rem; + font-weight: 500; + line-height: 1.25rem; + letter-spacing: -0.01094rem; + } + + .tab-style:hover { + color: var(--c-slate-11); + } + + .hover-card-shadow:hover { + background: var(0 3px 6px 0 rgba(0, 0, 0, 0.03), + 0 1px 0 0 #FFF inset, + 0 1px 0 0 rgba(0, 0, 0, 0.04), + 0 0 0 1px rgba(0, 0, 0, 0.08), linear-gradient(180deg, var(--secondary-1, #FCFCFD) 0%, var(--white-1, #FFF) 100%)); + box-shadow: 0 3px 6px 0 rgba(0, 0, 0, 0.03), 0 1px 0 0 #FFF inset, 0 1px 0 0 rgba(0, 0, 0, 0.04), 0 0 0 1px rgba(0, 0, 0, 0.08); + } + + :where(.dark, .dark *) .hover-card-shadow:hover { + background: var(--m-slate-10, #21252B); + box-shadow: 0 0 0 1px var(--m-slate-9, #2A3037); + } + + .navbar-shadow { + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.03), 0 -1px 1px 0 rgba(0, 0, 0, 0.04), 0 16px 32px 0 rgba(0, 0, 0, 0.08), 0 1px 1px 0 rgba(0, 0, 0, 0.08), 0 4px 8px 0 rgba(0, 0, 0, 0.03); + } + + :where(.dark, .dark *) .navbar-shadow { + box-shadow: 0 0 0 1px var(--m-slate-9, #2A3037); + } + + .tab-style[data-state='active'] { + color: var(--c-violet-9); + } + + .tab-style:not([data-state='active']) { + color: var(--c-slate-12); + } + + .demo-code-block { + width: 100% !important; + height: auto !important; + margin: 0 !important; + background: transparent !important; + color: var(--c-slate-9) !important; + resize: none !important; + outline: none !important; + scrollbar-width: thin !important; + font-family: "JetBrains Mono" !important; + } + + .demo-code-block>* { + background: transparent !important; + font-family: "JetBrains Mono" !important; + font-size: 0.8125rem !important; + font-style: normal !important; + font-weight: 400 !important; + line-height: 150% !important; + letter-spacing: -0.01219rem !important; + } + + .demo-code-block pre { + background: transparent !important; + font-family: var(--font-jetbrains) !important; + font-size: 0.8125rem !important; + font-style: normal !important; + font-weight: 400 !important; + line-height: 150% !important; + letter-spacing: -0.01219rem !important; + padding: 2.5rem !important; + } + + .demo-code-block code { + font-family: var(--font-jetbrains) !important; + padding: 0 !important; + font-size: 0.8125rem !important; + font-weight: 400 !important; + line-height: 150% !important; + letter-spacing: -0.01219rem !important; + color: var(--c-slate-12) !important; + background-color: transparent !important; + } + + .font-small { + font-family: var(--font-instrument-sans); + font-size: 0.9rem; + font-style: normal; + font-weight: 500; + line-height: 1.25rem; + /* 142.857% */ + letter-spacing: -0.01094rem; + } + + .font-md { + font-family: var(--font-instrument-sans); + font-size: 1.125rem; + font-style: normal; + font-weight: 500; + line-height: 1.625rem; + letter-spacing: -0.01688rem; + } + + .font-smbold { + font-family: var(--font-instrument-sans); + font-size: 1rem; + font-style: normal; + font-weight: 600; + line-height: 1.5rem; + /* 150% */ + letter-spacing: -0.015rem; + + } + + .font-md-smbold { + font-family: var(--font-instrument-sans); + font-size: 1.125rem; + font-style: normal; + font-weight: 600; + line-height: 1.625rem; + letter-spacing: -0.01688rem; + } + + .font-small-smbold { + /* Small Semibold */ + font-family: var(--font-instrument-sans); + font-size: 0.875rem; + font-style: normal; + font-weight: 600; + line-height: 1.25rem; + /* 142.857% */ + letter-spacing: -0.01094rem; + } + + .font-base { + font-family: var(--font-instrument-sans); + font-size: 1rem; + font-style: normal; + font-weight: 500; + line-height: 1.5rem; + /* 150% */ + letter-spacing: -0.015rem; + } + + .font-large { + font-family: "Instrument Sans"; + font-size: 1.5rem; + font-style: normal; + font-weight: 600; + line-height: 2rem; + /* 133.333% */ + letter-spacing: -0.03rem; + } + + .font-x-large { + font-family: var(--font-instrument-sans); + font-size: 2rem; + font-style: normal; + font-weight: 600; + line-height: 2.5rem; + /* 125% */ + letter-spacing: -0.06rem; + } + + .font-xx-large { + font-family: var(--font-instrument-sans); + font-size: 3rem; + font-style: normal; + font-weight: 600; + line-height: 3.5rem; + /* 116.667% */ + letter-spacing: -0.15rem; + } + + .font-xxx-large { + font-family: var(--font-instrument-sans); + font-size: 3.5rem; + font-style: normal; + font-weight: 600; + line-height: 4rem; + /* 114.286% */ + letter-spacing: -0.175rem; + } + + .v-link { + color: var(--c-violet-9); + } + + .hidden-scrollbar::-webkit-scrollbar { + background-color: transparent; + } + + .hidden-scrollbar::-webkit-scrollbar-thumb { + background-color: transparent; + } + + /* Hide scrollbar for Chrome, Safari and Opera */ + .no-scrollbar::-webkit-scrollbar { + display: none; + } + + /* Hide scrollbar for IE, Edge and Firefox */ + .no-scrollbar { + -ms-overflow-style: none; + /* IE and Edge */ + scrollbar-width: none; + /* Firefox */ + } + + @supports ((-webkit-touch-callout: none) or (font: -apple-system-body) or (-moz-appearance: none)) { + + .safari-nav-positioner, + .safari-nav-positioner[data-slot='navigation-menu-positioner'] { + transition: none !important; + transition-property: none !important; + transition-duration: 0ms !important; + } + } +} + +@property --border-angle { + inherits: false; + initial-value: 0deg; + syntax: ''; +} + +@layer base { + + button, + [role='button'] { + cursor: pointer; + } + + button:disabled, + [role='button']:disabled { + cursor: default; + } + + body { + @apply isolate bg-slate-1 font-sans antialiased; + } +} \ No newline at end of file diff --git a/shared/styles/shadows.py b/shared/styles/shadows.py new file mode 100644 index 0000000..1b246cd --- /dev/null +++ b/shared/styles/shadows.py @@ -0,0 +1,7 @@ +# SHADOWS + +shadows = { + "small": "0px 2px 4px 0px rgba(28, 32, 36, 0.05);", + "medium": "0px 4px 8px 0px rgba(28, 32, 36, 0.04);", + "large": "0px 24px 12px 0px rgba(28, 32, 36, 0.02), 0px 8px 8px 0px rgba(28, 32, 36, 0.02), 0px 2px 6px 0px rgba(28, 32, 36, 0.02);", +} diff --git a/shared/styles/styles.py b/shared/styles/styles.py new file mode 100644 index 0000000..f7c6c36 --- /dev/null +++ b/shared/styles/styles.py @@ -0,0 +1,72 @@ +"""App styling.""" + +import reflex as rx + +import shared.styles.fonts as fonts +from shared.styles.colors import c_color + +font_weights = { + "bold": "800", + "heading": "700", + "subheading": "600", + "section": "600", +} + + +def get_code_style(color: str): + return { + "color": c_color(color, 9), # type: ignore[arg-type] + "border_radius": "4px", + "border": f"1px solid {c_color(color, 4)}", # type: ignore[arg-type] + "background": c_color(color, 3), # type: ignore[arg-type] + **fonts.code, + "line_height": "1.5", + } + + +def get_code_style_rdx(color: str): # type: ignore[reportArgumentType] + return { + "color": rx.color(color, 11), # type: ignore[reportArgumentType] + "border_radius": "0.25rem", + "border": f"1px solid {rx.color(color, 5)}", # type: ignore[reportArgumentType] + "background": rx.color(color, 3), # type: ignore[reportArgumentType] + } + + +cell_style = { + **fonts.small, + "color": c_color("slate", 11), + "line_height": "1.5", +} + + +# General styles. +SANS = "Instrument Sans" +BOLD_WEIGHT = font_weights["bold"] + +DOC_BORDER_RADIUS = "6px" + +# The base application style. +BASE_STYLE = { + "background_color": "var(--c-slate-1)", + "::selection": { + "background_color": rx.color("accent", 5, True), + }, + "font_family": SANS, + rx.heading: { + "font_family": SANS, + }, + rx.divider: {"margin_bottom": "1em", "margin_top": "0.5em"}, + rx.vstack: {"align_items": "center"}, + rx.hstack: {"align_items": "center"}, + rx.markdown: { + "background": "transparent", + }, +} + +# Fonts to include. +STYLESHEETS = [ + "fonts.css", + "custom-colors.css", + "tailwind-theme.css", +] diff --git a/shared/telemetry/__init__.py b/shared/telemetry/__init__.py new file mode 100644 index 0000000..7d4a13b --- /dev/null +++ b/shared/telemetry/__init__.py @@ -0,0 +1 @@ +from .pixels import get_pixel_website_trackers as get_pixel_website_trackers diff --git a/shared/telemetry/pixels.py b/shared/telemetry/pixels.py new file mode 100644 index 0000000..a3934ad --- /dev/null +++ b/shared/telemetry/pixels.py @@ -0,0 +1,22 @@ +"""This module contains the pixel trackers for the website.""" + +import reflex as rx + +from reflex_ui.blocks.telemetry import ( + get_default_telemetry_script, + get_google_analytics_trackers, + get_unify_trackers, + gtag_report_conversion, +) + + +def get_pixel_website_trackers() -> list[rx.Component]: + """Get the pixel trackers for the website.""" + return [ + *get_google_analytics_trackers(tracking_id="G-4T7C8ZD9TR"), + gtag_report_conversion( + conversion_id_and_label="AW-11360851250/ASB4COvpisIbELKqo6kq" + ), + get_unify_trackers(), + get_default_telemetry_script(), + ] diff --git a/shared/templates/__init__.py b/shared/templates/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/shared/templates/gallery_app_page.py b/shared/templates/gallery_app_page.py new file mode 100644 index 0000000..847d07e --- /dev/null +++ b/shared/templates/gallery_app_page.py @@ -0,0 +1,88 @@ +import functools +from collections.abc import Callable + +import reflex as rx + +from shared.route import Route + + +def gallery_app_page( + path: str, + title: str, + description: str, + image: str, + demo: str, + meta: list[dict[str, str]] | None = None, + props: dict | None = None, + add_as_page: bool = True, +) -> Callable: + """A template that most pages on the reflex.dev site should use. + + This template wraps the webpage with the navbar and footer. + + Args: + path: The path of the page. + title: The title of the page. + description: The description of the page. + image: The image of the page. + demo: The demo link of the app. + meta: The meta tags of the page. + props: Props to apply to the template. + add_as_page: whether to add the route to the app pages. + + Returns: + A wrapper function that returns the full webpage. + """ + props = props or {} + + def webpage(contents: Callable[[], Route]) -> Route: + """Wrapper to create a templated route. + + Args: + contents: The function to create the page route. + + Returns: + The templated route. + """ + + @functools.wraps(contents) + def wrapper(*children, **props) -> rx.Component: + """The template component. + + Args: + children: The children components. + props: The props to apply to the component. + + Returns: + The component with the template applied. + """ + # Import here to avoid circular imports. + from shared.views.footer import footer_index + from shared.views.marketing_navbar import marketing_navbar + + # Wrap the component in the template. + return rx.box( + marketing_navbar(), + rx.box( + rx.el.main( + contents(*children, **props), + class_name="w-full z-[1] relative flex flex-col mx-auto lg:border-x border-slate-3 pt-24 lg:pt-48", + ), + class_name="relative flex flex-col justify-start items-center w-full h-full min-h-screen font-instrument-sans mx-auto max-w-[64.19rem]", + ), + footer_index(), + class_name="relative overflow-hidden flex flex-col justify-center items-center w-full", + **props, + ) + + return Route( + path=path, + title=title.replace("_", " ").title() + " - Reflex App Template", + description=description, + meta=meta, + image=image, + component=wrapper, + add_as_page=add_as_page, + ) + + return webpage diff --git a/shared/templates/marketing_page.py b/shared/templates/marketing_page.py new file mode 100644 index 0000000..0f1435f --- /dev/null +++ b/shared/templates/marketing_page.py @@ -0,0 +1,99 @@ +import functools +from collections.abc import Callable + +import reflex as rx + +import reflex_ui as ui +from shared.components.hosting_banner import HostingBannerState +from shared.route import Route + +DEFAULT_TITLE = "The platform to build and scale enterprise apps" +DEFAULT_DESCRIPTION = "Build secure internal apps with AI. Deploy on prem or cloud with governance. Technical and nontechnical teams ship together." + + +def marketing_page( + path: str, + title: str = DEFAULT_TITLE, + description: str | None = DEFAULT_DESCRIPTION, + image: str | None = None, + meta: list[dict[str, str]] | None = None, + props: dict | None = None, + add_as_page: bool = True, +) -> Callable: + """A template that most pages on the reflex.dev site should use. + + This template wraps the webpage with the navbar and footer. + + Args: + path: The path of the page. + title: The title of the page. + description: The description of the page. + image: The image to use for social media. + meta: Additional meta tags to add to the page. + props: Props to apply to the template. + add_as_page: whether to add the route to the app pages. + + Returns: + A wrapper function that returns the full webpage. + """ + props = props or {} + + def marketing_page(contents: Callable[[], Route]) -> Route: + """Wrapper to create a templated route. + + Args: + contents: The function to create the page route. + + Returns: + The templated route. + """ + + @functools.wraps(contents) + def wrapper(*children, **props) -> rx.Component: + """The template component. + + Args: + children: The children components. + props: The props to apply to the component. + + Returns: + The component with the template applied. + """ + # Import here to avoid circular imports. + from shared.views.cta_card import cta_card + from shared.views.footer import footer_index + from shared.views.marketing_navbar import marketing_navbar + + # Wrap the component in the template. + return rx.el.div( + marketing_navbar(), + rx.el.main( + rx.el.div( + contents(*children, **props), + cta_card(), + footer_index(), + class_name="flex flex-col relative justify-center items-center w-full", + ), + class_name=ui.cn( + "flex flex-col w-full relative h-full justify-center items-center", + rx.cond( + HostingBannerState.is_banner_visible, + "mt-28", + "mt-16", + ), + ), + ), + class_name="flex flex-col w-full justify-center items-center relative dark:bg-m-slate-12 bg-m-slate-1", + ) + + return Route( + path=path, + title=title, + description=description, + image=image, + meta=meta, + component=wrapper, + add_as_page=add_as_page, + ) + + return marketing_page diff --git a/shared/templates/webpage.py b/shared/templates/webpage.py new file mode 100644 index 0000000..7ed143d --- /dev/null +++ b/shared/templates/webpage.py @@ -0,0 +1,91 @@ +import functools +from collections.abc import Callable + +import reflex as rx + +from shared.route import Route + +DEFAULT_TITLE = "The platform to build and scale enterprise apps" +DEFAULT_DESCRIPTION = "Build secure internal apps with AI. Deploy on prem or cloud with governance. Technical and nontechnical teams ship together." + + +def webpage( + path: str, + title: str = DEFAULT_TITLE, + description: str | None = DEFAULT_DESCRIPTION, + image: str | None = None, + meta: list[dict[str, str]] | None = None, + props: dict | None = None, + add_as_page: bool = True, +) -> Callable: + """A template that most pages on the reflex.dev site should use. + + This template wraps the webpage with the navbar and footer. + + Args: + path: The path of the page. + title: The title of the page. + description: The description of the page. + image: The image to use for social media. + meta: Additional meta tags to add to the page. + props: Props to apply to the template. + add_as_page: whether to add the route to the app pages. + + Returns: + A wrapper function that returns the full webpage. + """ + props = props or {} + + def webpage(contents: Callable[[], Route]) -> Route: + """Wrapper to create a templated route. + + Args: + contents: The function to create the page route. + + Returns: + The templated route. + """ + + @functools.wraps(contents) + def wrapper(*children, **props) -> rx.Component: + """The template component. + + Args: + children: The children components. + props: The props to apply to the component. + + Returns: + The component with the template applied. + """ + # Import here to avoid circular imports. + from shared.components.patterns import default_patterns + from shared.views.cta_card import cta_card + from shared.views.footer import footer_index + from shared.views.marketing_navbar import marketing_navbar + + # Wrap the component in the template. + return rx.box( + *default_patterns(), + marketing_navbar(), + rx.el.main( + contents(*children, **props), + rx.box(class_name="flex-grow"), + class_name="w-full z-[1]", + ), + cta_card(), + footer_index(), + class_name="relative flex flex-col justify-start items-center w-full h-full min-h-screen font-instrument-sans overflow-hidden", + **props, + ) + + return Route( + path=path, + title=title, + description=description, + image=image, + meta=meta, + component=wrapper, + add_as_page=add_as_page, + ) + + return webpage diff --git a/shared/utils/__init__.py b/shared/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/shared/utils/docpage.py b/shared/utils/docpage.py new file mode 100644 index 0000000..4b8c1dd --- /dev/null +++ b/shared/utils/docpage.py @@ -0,0 +1,172 @@ +"""Docpage utilities: TOC generation and sidebar highlight.""" + +import flexdown +import mistletoe + + +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: mistletoe.block_token.BlockToken): + """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)] + + 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: flexdown.Document, href: str, component_list: list | None = None): + from shared.components.blocks.flexdown import xd + from shared.constants import REFLEX_ASSETS_CDN + + component_list = component_list or [] + component_list = component_list[1:] + + env = source.metadata + env["__xd"] = xd + env["REFLEX_ASSETS_CDN"] = REFLEX_ASSETS_CDN + + doc_content = source.content + 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 + content = block.get_content(env) + content_pieces.append(content) + + content = "\n".join(content_pieces) + doc = mistletoe.Document(content) + + 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/views/__init__.py b/shared/views/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/shared/views/cta_card.py b/shared/views/cta_card.py new file mode 100644 index 0000000..d935bcc --- /dev/null +++ b/shared/views/cta_card.py @@ -0,0 +1,48 @@ +import reflex as rx + +import reflex_ui as ui +from reflex_ui.blocks.demo_form import demo_form_dialog +from shared.components.marketing_button import button as marketing_button +from shared.constants import REFLEX_ASSETS_CDN, REFLEX_BUILD_URL + + +@rx.memo +def cta_card(): + return rx.el.div( + rx.el.div( + rx.el.span( + "The Unified Platform to Build and Scale Enterprise Apps", + class_name="text-slate-12 lg:text-3xl text-2xl font-[575]", + ), + rx.el.span( + "Describe your idea, and let AI transform it into a complete, production-ready Python web application.", + class_name="text-m-slate-7 dark:text-m-slate-6 text-sm font-medium", + ), + rx.el.div( + demo_form_dialog( + trigger=marketing_button( + "Book a Demo", + variant="primary", + ), + ), + rx.el.a( + marketing_button( + "Try for free", + ui.icon("ArrowRight01Icon"), + variant="ghost", + ), + to=REFLEX_BUILD_URL, + target="_blank", + ), + class_name="flex flex-row gap-4 items-center", + ), + class_name="flex flex-col gap-6 justify-center max-w-[24.5rem]", + ), + rx.image( + f"{REFLEX_ASSETS_CDN}common/{rx.color_mode_cond('light', 'dark')}/cta.svg", + class_name="w-auto h-full pointer-events-none", + loading="lazy", + alt="CTA Card", + ), + class_name="flex flex-row justify-between max-w-(--docs-layout-max-width) mx-auto w-full bg-white/96 dark:bg-m-slate-11 backdrop-blur-[16px] rounded-xl relative overflow-hidden shadow-[0_0_0_1px_rgba(0,0,0,0.04),0_12px_24px_0_rgba(0,0,0,0.08),0_1px_1px_0_rgba(0,0,0,0.01),0_4px_8px_0_rgba(0,0,0,0.03)] dark:shadow-none dark:border dark:border-m-slate-9 pl-16 max-lg:hidden mb-12 mt-24", + ) diff --git a/shared/views/footer.py b/shared/views/footer.py new file mode 100644 index 0000000..35ea45e --- /dev/null +++ b/shared/views/footer.py @@ -0,0 +1,260 @@ +from datetime import datetime + +import reflex as rx +from reflex.style import color_mode, set_color_mode + +import reflex_ui as ui +from reflex_ui import button as marketing_button +from shared.backend.signup import IndexState +from shared.components.icons import get_icon +from shared.constants import ( + CHANGELOG_URL, + DISCORD_URL, + FORUM_URL, + GITHUB_URL, + LINKEDIN_URL, + REFLEX_ASSETS_CDN, + REFLEX_BUILD_URL, + ROADMAP_URL, + TWITTER_URL, +) + + +def ph_1() -> rx.Component: + return rx.fragment( + rx.image( + src=f"{REFLEX_ASSETS_CDN}logos/dark/ph_1.svg", + class_name="hidden dark:block h-[36px] w-fit", + alt="1st product of the day logo", + loading="lazy", + ), + rx.image( + src=f"{REFLEX_ASSETS_CDN}logos/light/ph_1.svg", + class_name="dark:hidden block h-[36px] w-fit", + alt="1st product of the day logo", + loading="lazy", + ), + ) + + +def logo() -> rx.Component: + return rx.el.a( + rx.image( + src=f"{REFLEX_ASSETS_CDN}logos/light/reflex.svg", + alt="Reflex Logo", + class_name="shrink-0 block dark:hidden", + ), + rx.image( + src=f"{REFLEX_ASSETS_CDN}logos/dark/reflex.svg", + alt="Reflex Logo", + class_name="shrink-0 hidden dark:block", + ), + to="/", + class_name="block shrink-0 mr-[7rem] md:hidden xl:block", + ) + + +def tab_item(mode: str, icon: str) -> rx.Component: + active_cn = " shadow-[0_-1px_0_0_rgba(0,0,0,0.08)_inset,0_0_0_1px_rgba(0,0,0,0.08)_inset,0_1px_2px_0_rgba(0,0,0,0.02),0_1px_4px_0_rgba(0,0,0,0.02)] dark:shadow-[0_1px_0_0_rgba(255,255,255,0.16)_inset] bg-white dark:bg-m-slate-10 hover:bg-m-slate-2 dark:hover:bg-m-slate-9 text-m-slate-12 dark:text-m-slate-3" + unactive_cn = " hover:text-m-slate-12 dark:hover:text-m-slate-3 text-m-slate-7 dark:text-m-slate-6" + return rx.el.button( + get_icon(icon, class_name="shrink-0"), + on_click=set_color_mode(mode), # type: ignore[reportArgumentType] + class_name=ui.cn( + "flex items-center cursor-pointer justify-center rounded-lg transition-colors size-7 outline-none focus:outline-none ", + rx.cond(mode == color_mode, active_cn, unactive_cn), + ), + custom_attrs={"aria-label": f"Toggle {mode} color mode"}, + ) + + +def dark_mode_toggle() -> rx.Component: + return rx.box( + tab_item("system", "computer_footer"), + tab_item("light", "sun_footer"), + tab_item("dark", "moon_footer"), + class_name="flex flex-row gap-0.5 items-center p-0.5 [box-shadow:0_1px_0_0_rgba(0,_0,_0,_0.08),_0_0_0_1px_rgba(0,_0,_0,_0.08),_0_1px_2px_0_rgba(0,_0,_0,_0.02),_0_1px_4px_0_rgba(0,_0,_0,_0.02)] w-fit mt-auto bg-m-slate-1 dark:bg-m-slate-12 rounded-[0.625rem] dark:border dark:border-m-slate-9 border border-transparent lg:ml-auto mr-px", + ) + + +def footer_link(text: str, href: str) -> rx.Component: + return rx.el.a( + text, + rx.icon( + tag="chevron-right", + size=16, + class_name="shrink-0 lg:hidden flex", + ), + to=href, + target="_blank", + class_name="font-[525] text-m-slate-7 hover:text-m-slate-8 dark:hover:text-m-slate-5 dark:text-m-slate-6 text-sm transition-color w-full lg:w-fit flex flex-row justify-between items-center", + ) + + +def footer_link_flex( + heading: str, links: list[rx.Component], class_name: str = "" +) -> rx.Component: + return rx.el.div( + rx.el.h3( + heading, + class_name="text-xs text-m-slate-12 dark:text-m-slate-3 font-semibold w-fit mb-3", + ), + *links, + class_name=ui.cn("flex flex-col gap-2", class_name), + ) + + +def social_menu_item(icon: str, url: str, name: str) -> rx.Component: + return rx.el.a( + marketing_button( + get_icon(icon, class_name="shrink-0"), + variant="ghost", + size="icon-sm", + class_name="text-m-slate-7 dark:text-m-slate-6", + native_button=False, + ), + to=url, + custom_attrs={"aria-label": "Social link for " + name}, + target="_blank", + ) + + +def menu_socials() -> rx.Component: + return rx.box( + social_menu_item("twitter_footer", TWITTER_URL, "Twitter"), + social_menu_item("github_navbar", GITHUB_URL, "Github"), + social_menu_item("discord_navbar", DISCORD_URL, "Discord"), + social_menu_item("linkedin_footer", LINKEDIN_URL, "LinkedIn"), + social_menu_item("forum_footer", FORUM_URL, "Forum"), + class_name="flex flex-row items-center gap-2", + ) + + +def newsletter_input() -> rx.Component: + return rx.box( + rx.cond( + IndexState.signed_up, + rx.box( + rx.box( + rx.icon( + tag="circle-check", + size=16, + class_name="!text-violet-9", + ), + rx.text( + "Thanks for subscribing!", + class_name="text-xs text-m-slate-7 dark:text-m-slate-6 font-semibold", + ), + class_name="flex flex-row items-center gap-2", + ), + marketing_button( + "Sign up for another email", + variant="outline", + size="sm", + on_click=IndexState.signup_for_another_user, + ), + class_name="flex flex-col flex-wrap gap-2", + ), + rx.form( + rx.el.input( + placeholder="Email", + name="input_email", + type="email", + required=True, + class_name="relative [box-shadow:0_-1px_0_0_rgba(0,_0,_0,_0.08)_inset,_0_0_0_1px_rgba(0,_0,_0,_0.08)_inset,_0_1px_2px_0_rgba(0,_0,_0,_0.02),_0_1px_4px_0_rgba(0,_0,_0,_0.02)] rounded-lg h-8 px-2 py-1.5 w-[12rem] text-sm placeholder:text-m-slate-7 dark:placeholder:text-m-slate-6 font-[525] focus:outline-none outline-none dark:border dark:border-m-slate-9 dark:bg-m-slate-11", + ), + marketing_button( + "Subscribe", + type="submit", + variant="outline", + size="sm", + class_name="w-fit max-w-full", + ), + class_name="w-full flex flex-col lg:flex-row gap-2 lg:items-center items-start", + on_submit=IndexState.signup, + ), + ), + class_name="w-full", + ) + + +def newsletter() -> rx.Component: + return rx.el.div( + rx.text( + "Get updates", + class_name="text-xs text-m-slate-7 dark:text-m-slate-6 font-semibold", + ), + newsletter_input(), + class_name="flex flex-col items-start gap-4 self-stretch", + ) + + +@rx.memo +def footer_index(class_name: str = "", grid_class_name: str = "") -> rx.Component: + return rx.el.footer( + rx.el.div( + logo(), + rx.el.div( + footer_link_flex( + "Product", + [ + footer_link("AI Builder", REFLEX_BUILD_URL), + footer_link("Framework", "/framework"), + footer_link("Cloud", "/cloud"), + ], + ), + footer_link_flex( + "Solutions", + [ + footer_link("Enterprise", "/use-cases"), + footer_link("Finance", "/use-cases/finance"), + footer_link("Healthcare", "/use-cases/healthcare"), + footer_link("Consulting", "/use-cases/consulting"), + footer_link("Government", "/use-cases/government"), + ], + ), + footer_link_flex( + "Resources", + [ + footer_link("Documentation", "/docs"), + footer_link("FAQ", "/faq"), + footer_link("Common Errors", "/errors"), + footer_link("Roadmap", ROADMAP_URL), + footer_link("Changelog", CHANGELOG_URL), + ], + ), + rx.el.div( + class_name="absolute -top-24 -right-px w-px h-24 bg-gradient-to-b from-transparent to-current text-m-slate-4 dark:text-m-slate-10 max-lg:hidden" + ), + class_name=ui.cn( + "grid grid-cols-1 lg:grid-cols-3 gap-12 w-full lg:pr-12 pb-8 lg:border-r border-m-slate-4 dark:border-m-slate-10 xl:ml-auto relative", + grid_class_name, + ), + ), + rx.el.div( + newsletter(), + ph_1(), + dark_mode_toggle(), + class_name="flex flex-col gap-6 lg:pl-12 pb-8 max-lg:justify-start", + ), + class_name="flex flex-col max-lg:gap-6 lg:flex-row w-full", + ), + rx.el.div( + rx.el.span( + f"Copyright © {datetime.now().year} Pynecone, Inc.", + class_name="text-xs text-m-slate-7 dark:text-m-slate-6 font-medium", + ), + menu_socials(), + rx.el.div( + class_name="absolute -top-px -right-24 w-24 h-px bg-gradient-to-l from-transparent to-current text-m-slate-4 dark:text-m-slate-10 max-lg:hidden" + ), + rx.el.div( + class_name="absolute -top-px -left-24 w-24 h-px bg-gradient-to-r from-transparent to-current text-m-slate-4 dark:text-m-slate-10 max-lg:hidden" + ), + class_name="flex flex-row items-center justify-between py-6 gap-4 w-full border-t border-m-slate-4 dark:border-m-slate-10 relative", + ), + class_name=ui.cn( + "flex flex-col max-w-(--docs-layout-max-width) justify-center items-center w-full mx-auto mt-24 max-lg:px-4 overflow-hidden", + class_name, + ), + ) diff --git a/shared/views/hosting_banner.py b/shared/views/hosting_banner.py new file mode 100644 index 0000000..dbdede3 --- /dev/null +++ b/shared/views/hosting_banner.py @@ -0,0 +1,129 @@ +import datetime + +import reflex as rx + +import reflex_ui as ui +from shared.constants import REFLEX_ASSETS_CDN + + +def glow() -> rx.Component: + return rx.box( + class_name="absolute w-[120rem] h-[23.75rem] flex-shrink-0 rounded-[120rem] left-1/2 -translate-x-1/2 z-[0] top-[-16rem] dark:[background-image:radial-gradient(50%_50%_at_50%_50%,_rgba(58,45,118,1)_0%,_rgba(21,22,24,0.00)_100%)] [background-image:radial-gradient(50%_50%_at_50%_50%,_rgba(235,228,255,0.95)_0%,_rgba(252,252,253,0.00)_100%)] saturate-200 dark:saturate-100 group-hover:saturate-300 transition-[saturate] dark:group-hover:saturate-100", + ) + + +POST_LINK = "https://www.producthunt.com/products/reflex-5?launch=reflex-7" +BLOG_LINK = "/blog/on-premises-deployment" + +# October 25, 2025 12:01 AM PDT (UTC-7) = October 25, 2025 07:01 AM UTC +DEADLINE = datetime.datetime(2025, 10, 25, 7, 1, tzinfo=datetime.UTC) + + +class HostingBannerState(rx.State): + show_banner: rx.Field[bool] = rx.field(True) + force_hide_banner: rx.Field[bool] = rx.field(False) + + @rx.event + def hide_banner(self): + self.force_hide_banner = True + + @rx.event + def check_deadline(self): + if datetime.datetime.now(datetime.UTC) < DEADLINE: + self.show_banner = True + + @rx.event + def show_blog_banner(self): + """Show the on-premises blog banner.""" + self.show_banner = True + + @rx.var + def is_banner_visible(self) -> bool: + return self.show_banner and not self.force_hide_banner + + +def timer(): + remove_negative_sign = rx.vars.function.ArgsFunctionOperation.create( + args_names=("t",), + return_expr=rx.vars.sequence.string_replace_operation( + rx.Var("t").to(str), "-", "" + ), + ) + + return rx.el.div( + rx.moment( + date=DEADLINE, + duration_from_now=True, + format="DD[d] HH[h] mm[m] ss[s]", + custom_attrs={"filter": remove_negative_sign}, + interval=1000, + class_name="font-medium text-sm", + ), + class_name="items-center gap-1 z-[1] bg-orange-4 border border-orange-5 rounded-md px-1.5 py-0.5 text-orange-11 font-medium text-sm md:flex hidden", + ) + + +def hosting_banner() -> rx.Component: + return rx.el.div( + rx.cond( + HostingBannerState.is_banner_visible, + rx.el.div( + rx.el.a( + rx.box( + rx.image( + src=f"{REFLEX_ASSETS_CDN}common/{rx.color_mode_cond('light', 'dark')}/squares_banner.svg", + alt="Square Banner", + class_name="pointer-events-none absolute -left-[16rem] max-lg:hidden", + ), + rx.box( + # Header text with responsive spans + rx.el.span( + "New", + class_name="items-center font-[525] px-2.5 h-7 rounded-lg text-sm text-m-slate-3 z-[1] max-lg:hidden lg:inline-flex border border-white/16", + ), + rx.el.span( + "Reflex Build On-Prem: A Secure Builder Running in Your Environment", + rx.el.span( + ". Learn more", + class_name="lg:hidden text-m-slate-6 dark:text-m-slate-2", + ), + class_name="text-m-slate-3 font-[525] text-sm lg:text-nowrap inline-block", + ), + rx.el.span( + class_name="w-px h-7 bg-gradient-to-b from-transparent via-white/24 to-transparent max-lg:hidden", + ), + ui.button( + "Learn more", + ui.icon("ArrowRight01Icon"), + variant="ghost", + size="xs", + aria_label="Learn more", + class_name="text-m-slate-3 dark:hover:text-m-slate-5 max-lg:hidden", + ), + class_name="flex flex-row items-center md:gap-4 gap-2", + ), + rx.image( + src=f"{REFLEX_ASSETS_CDN}common/{rx.color_mode_cond('light', 'dark')}/squares_banner.svg", + alt="Square Banner", + class_name="pointer-events-none absolute -right-[16rem] max-lg:hidden", + ), + class_name="flex flex-row items-center relative", + ), + to=BLOG_LINK, + is_external=False, + class_name="flex justify-start md:justify-center md:col-start-2 max-w-[73rem]", + ), + rx.el.button( + ui.icon( + "MultiplicationSignIcon", + ), + aria_label="Close banner", + type="button", + class_name="cursor-pointer hover:text-m-slate-5 transition-colors text-m-slate-3 z-10 size-10 flex items-center justify-center shrink-0 md:col-start-3 justify-self-end ml-auto", + on_click=HostingBannerState.hide_banner, + ), + class_name="px-5 lg:px-0 w-screen min-h-[2rem] lg:h-10 flex md:grid md:grid-cols-[1fr_auto_1fr] items-center bg-m-slate-12 dark:bg-[#6550B9] gap-4 overflow-hidden relative lg:py-0 py-2 max-w-full group", + ), + ), + on_mount=HostingBannerState.show_blog_banner, + ) diff --git a/shared/views/marketing_navbar.py b/shared/views/marketing_navbar.py new file mode 100644 index 0000000..0ed5264 --- /dev/null +++ b/shared/views/marketing_navbar.py @@ -0,0 +1,558 @@ +import reflex as rx + +import reflex_ui as ui +from reflex_ui.blocks.demo_form import demo_form_dialog +from shared.backend.get_blogs import BlogPostDict, RecentBlogsState +from shared.components.icons import get_icon +from shared.components.marketing_button import button as marketing_button +from shared.components.marquee import marquee +from shared.constants import ( + CHANGELOG_URL, + CONTRIBUTING_URL, + DISCUSSIONS_URL, + GITHUB_STARS, + GITHUB_URL, + JOBS_BOARD_URL, + REFLEX_ASSETS_CDN, + REFLEX_BUILD_URL, +) +from shared.views.sidebar import navbar_sidebar_button + + +def social_proof_card(image: str) -> rx.Component: + return rx.el.div( + rx.image( + f"{REFLEX_ASSETS_CDN}companies/{rx.color_mode_cond('light', 'dark')}/{image}_small.svg", + loading="lazy", + alt=f"{image} logo", + class_name="w-auto h-fit pointer-events-none", + ), + class_name="flex justify-center items-center px-3", + ) + + +def logos_carousel() -> rx.Component: + logos = [ + "agricole", + "man", + "shell", + "red_hat", + "accenture", + "dell", + "microsoft", + "world", + "ford", + "unicef", + "nike", + ] + return marquee( + *[social_proof_card(logo) for logo in logos], + direction="left", + gradient_color="light-dark(var(--c-white-1), var(--c-m-slate-11))", + class_name="h-[1.625rem] w-full overflow-hidden mt-auto", + gradient_width=65, + speed=25, + pause_on_hover=False, + ) + + +def github() -> rx.Component: + return rx.el.a( + marketing_button( + get_icon(icon="github_navbar", class_name="shrink-0"), + f"{GITHUB_STARS // 1000}K", + custom_attrs={ + "aria-label": f"View Reflex on GitHub - {GITHUB_STARS // 1000}K stars" + }, + size="sm", + variant="ghost", + ), + to=GITHUB_URL, + custom_attrs={ + "aria-label": f"View Reflex on GitHub - {GITHUB_STARS // 1000}K stars" + }, + ) + + +def logo() -> rx.Component: + return rx.el.a( + rx.image( + src=f"{REFLEX_ASSETS_CDN}logos/light/reflex.svg", + alt="Reflex Logo", + class_name="shrink-0 block dark:hidden", + ), + rx.image( + src=f"{REFLEX_ASSETS_CDN}logos/dark/reflex.svg", + alt="Reflex Logo", + class_name="shrink-0 hidden dark:block", + ), + to="/", + class_name="block shrink-0 lg:mr-9", + ) + + +def menu_trigger(title: str, content: rx.Component) -> rx.Component: + return ui.navigation_menu.item( + ui.navigation_menu.trigger( + marketing_button( + title, + ui.icon( + "ArrowDown01Icon", class_name="chevron transition-all ease-out" + ), + size="sm", + variant="ghost", + class_name="font-[550] menu-button", + native_button=False, + ), + style={ + "&[data-popup-open] .chevron": { + "transform": "rotate(180deg)", + }, + "&[data-popup-open] .menu-button": { + "color": "light-dark(var(--primary-10), var(--primary-9))", + }, + }, + class_name="px-1", + aria_label=f"{title} menu", + unstyled=True, + ), + content, + value=title, + class_name="cursor-pointer xl:flex hidden h-full items-center justify-center", + unstyled=True, + custom_attrs={"role": "menuitem"}, + ) + + +def menu_content(content: rx.Component, class_name: str = "") -> rx.Component: + return ui.navigation_menu.content( + content, + unstyled=True, + class_name=ui.cn( + "data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 ease-[cubic-bezier(0.22,1,0.36,1)] group-data-[viewport=false]/navigation-menu:data-open:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-closed:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-open:fade-in-0 group-data-[viewport=false]/navigation-menu:data-closed:fade-out-0 group-data-[viewport=false]/navigation-menu:duration-300 data-[ending-style]:data-[activation-direction=left]:translate-x-[50%] data-[ending-style]:data-[activation-direction=right]:translate-x-[-50%] data-[starting-style]:data-[activation-direction=left]:translate-x-[-50%] data-[starting-style]:data-[activation-direction=right]:translate-x-[50%] w-max transition-[opacity,transform,translate] duration-[0.35s] data-[ending-style]:opacity-0 data-[starting-style]:opacity-0 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none", + "flex flex-row rounded-xl font-sans p-0", + class_name, + ), + keep_mounted=True, + ) + + +def platform_item(image: str, title: str, description: str, href: str) -> rx.Component: + return rx.el.div( + rx.image( + src=f"{REFLEX_ASSETS_CDN}common/{rx.color_mode_cond('light', 'dark')}/{image}", + alt=title, + class_name="size-18", + ), + rx.el.div( + rx.el.span( + title, + class_name="dark:text-m-slate-3 text-m-slate-12 text-sm font-[525]", + ), + rx.el.span( + description, + class_name="dark:text-m-slate-6 text-m-slate-7 text-sm font-[475]", + ), + class_name="flex flex-col", + ), + rx.el.a(class_name="absolute inset-0", to=href), + class_name="p-4 flex flex-row gap-6 relative cursor-pointer rounded-sm hover-card-shadow", + ) + + +def platform_content() -> rx.Component: + return menu_content( + rx.el.div( + rx.el.div( + rx.el.div( + rx.el.div( + rx.el.span( + "AI Builder", + class_name="dark:text-m-slate-3 text-m-slate-12 text-lg font-semibold mb-2", + ), + rx.el.span( + "Build production-ready web apps for your team in seconds with AI-powered code generation.", + class_name="dark:text-m-slate-6 text-m-slate-7 text-sm font-medium", + ), + class_name="p-4 flex flex-col relative hover-card-shadow rounded-md", + ), + rx.image( + src=f"{REFLEX_ASSETS_CDN}common/{rx.color_mode_cond('light', 'dark')}/ai_builder_pattern.svg", + alt="AI Builder Navbar Pattern", + class_name="pointer-events-none", + ), + rx.el.a( + class_name="absolute inset-0", + to=REFLEX_BUILD_URL, + target="_blank", + ), + class_name="relative flex flex-col hover-card-shadow rounded-md", + ), + class_name="p-4 flex flex-col rounded-xl bg-white-1 dark:bg-m-slate-11 h-full shadow-card dark:shadow-card-dark dark:border-r dark:border-m-slate-9", + ), + rx.el.div( + platform_item( + "framework_pixel.svg", + "Reflex Framework", + "Iterate on full-stack apps in pure Python. No JavaScript required.", + "/docs/getting-started/introduction/", + ), + platform_item( + "cloud_pixel.svg", + "Cloud Hosting", + "Deploy your app with a single command to Reflex Cloud.", + "/hosting/", + ), + rx.el.div( + rx.el.span( + "Reflex Is The Operating System ", + rx.el.br(), + " for Enterprise Apps", + class_name="dark:text-m-slate-6 text-m-slate-7 font-mono font-[415] text-[0.75rem] leading-4.5 uppercase", + ), + rx.image( + src=f"{REFLEX_ASSETS_CDN}common/{rx.color_mode_cond('light', 'dark')}/squares_navbar.svg", + alt="Squares Navbar", + class_name="absolute bottom-4 right-4 pointer-events-none", + ), + class_name="relative p-4", + ), + class_name="p-4 flex flex-col h-full", + ), + class_name="w-[46.5rem] grid grid-cols-2", + ), + ) + + +def solutions_item(title: str, icon: str, href: str) -> rx.Component: + return rx.el.a( + ui.icon( + icon, + class_name="shrink-0 text-m-slate-7 dark:text-m-slate-6 size-4.5", + ), + title, + to=href, + class_name="flex flex-row px-4 py-2 rounded-sm text-sm font-[525] text-m-slate-12 dark:text-m-slate-3 gap-3 items-center justify-start cursor-pointer hover-card-shadow", + ) + + +def solutions_column(title: str, items: list[tuple[str, str, str]]) -> rx.Component: + return rx.el.div( + rx.el.div( + rx.el.span( + title, + class_name="font-mono font-[415] text-[0.75rem] leading-4 uppercase pb-4 border-b border-dashed dark:border-m-slate-8 border-m-slate-6 dark:text-m-slate-6 text-m-slate-7", + ), + class_name="px-4 pt-4 flex flex-col", + ), + rx.el.div( + *[solutions_item(item[0], item[1], item[2]) for item in items], + class_name="flex flex-col", + ), + class_name="flex flex-col gap-4", + ) + + +def blog_item(post: BlogPostDict) -> rx.Component: + return rx.el.div( + rx.el.div( + rx.moment( + post["date"], + format="MMM DD YYYY", + class_name="text-m-slate-7 dark:text-m-slate-6 text-xs font-[415] font-mono uppercase text-nowrap", + ), + rx.image( + src=f"{REFLEX_ASSETS_CDN}common/{rx.color_mode_cond('light', 'dark')}/squares_blog.svg", + class_name="pointer-events-none", + alt="Squares Blog", + ), + class_name="flex flex-row items-center justify-start gap-6", + ), + rx.el.span( + post["title"], + class_name="dark:text-m-slate-3 text-m-slate-12 text-sm font-[525] group-hover:text-primary-10 dark:group-hover:text-primary-9 line-clamp-3", + ), + rx.el.a( + to=post["url"], + class_name="absolute inset-0", + ), + class_name="relative group flex flex-col gap-2 mb-2", + ) + + +def blog_column() -> rx.Component: + return rx.el.div( + rx.foreach( + RecentBlogsState.posts[:2], + blog_item, + ), + rx.el.a( + "Read All in Blog", + ui.icon("ArrowRight01Icon", class_name="ml-auto"), + to="/blog", + class_name="dark:text-m-slate-3 text-m-slate-12 text-sm font-[525] h-10 flex items-center justify-start gap-2 hover:text-primary-10 dark:hover:text-primary-9 mt-auto", + ), + on_mount=RecentBlogsState.fetch_recent_blogs, + class_name="flex flex-col gap-6 p-4 h-full", + ) + + +def customers_column() -> rx.Component: + return rx.el.div( + rx.el.div( + rx.el.div( + rx.el.span( + "Customers", + class_name="font-mono font-[415] text-[0.75rem] leading-4 uppercase pb-4 border-b border-dashed dark:border-m-slate-8 border-m-slate-6 dark:text-m-slate-6 text-m-slate-7", + ), + class_name="px-4 pt-4 flex flex-col", + ), + rx.el.div( + rx.el.span( + "Read Stories How Teams Use Reflex", + class_name="text-m-slate-12 dark:text-m-slate-3 text-lg font-[575]", + ), + rx.el.span( + "Discover how companies build internal tools, AI apps, and production dashboards in pure Python.", + class_name="text-m-slate-7 dark:text-m-slate-6 text-sm font-[475]", + ), + logos_carousel(), + class_name="flex flex-col gap-2 px-4 pb-4 h-full", + ), + rx.el.a(class_name="absolute inset-0", to="/customers/"), + class_name="flex flex-col gap-6 hover-card-shadow rounded-lg relative h-full hover:[--m-slate-11:var(--m-slate-10)] hover:shadow-card dark:hover:shadow-card-dark", + ), + class_name="p-4 block rounded-lg shadow-card dark:shadow-card-dark z-[1] bg-white-1 dark:bg-m-slate-11 dark:border-x dark:border-m-slate-9", + ) + + +def solutions_content() -> rx.Component: + return menu_content( + rx.el.div( + rx.el.div( + rx.el.div( + solutions_column( + "Who's It For", + [ + ("Executives", "LocationUser01Icon", "/use-cases/"), + ("Developers", "SourceCodeSquareIcon", "/use-cases/"), + ("Data Teams", "DatabaseIcon", "/use-cases/"), + ( + "Non Technical", + "CursorCircleSelection02Icon", + "/use-cases/", + ), + ], + ), + solutions_column( + "Industries", + [ + ("Enterprise", "OfficeIcon", "/use-cases/"), + ("Finance", "Wallet05Icon", "/use-cases/finance/"), + ("Healthcare", "HealthIcon", "/use-cases/healthcare/"), + ( + "Consulting", + "DocumentValidationIcon", + "/use-cases/consulting/", + ), + ( + "Government", + "BankIcon", + "/use-cases/government/", + ), + ], + ), + class_name="grid grid-cols-2", + ), + class_name="p-4 flex flex-col rounded-xl bg-white-1 dark:bg-m-slate-11 h-full w-[28rem] shadow-card dark:shadow-card-dark dark:border-r dark:border-m-slate-9", + ), + rx.el.div( + solutions_column( + "Migration", + [ + ( + "Switch from No Code", + "WebDesign01Icon", + "/migration/no-code/", + ), + ( + "Switch from Low Code", + "SourceCodeSquareIcon", + "/migration/low-code/", + ), + ( + "Switch from Other Frameworks", + "CodeIcon", + "/migration/other-frameworks/", + ), + ( + "Switch from Other AI tools", + "ArtificialIntelligence04Icon", + "/migration/other-ai-tools/", + ), + ], + ), + class_name="p-4 flex flex-col h-full", + ), + class_name="flex flex-row", + ), + ) + + +def resources_content() -> rx.Component: + return menu_content( + rx.el.div( + rx.el.div( + solutions_column( + "Developers", + [ + ("Templates", "Layout02Icon", "/templates/"), + ( + "Integrations", + "PlugSocketIcon", + "/docs/ai-builder/integrations/overview/", + ), + ("Changelog", "Clock02Icon", CHANGELOG_URL), + ("Contributing", "GitCommitIcon", CONTRIBUTING_URL), + ("Discussion", "BubbleChatIcon", DISCUSSIONS_URL), + ("FAQ", "HelpSquareIcon", "/faq/"), + ], + ), + class_name="p-4 flex flex-col rounded-xl bg-m-slate-1 dark:bg-m-slate-12 h-full", + ), + customers_column(), + rx.el.div( + blog_column(), + class_name="p-4 flex flex-col h-full bg-m-slate-1 dark:bg-m-slate-12", + ), + class_name="w-[52.5rem] grid grid-cols-3", + ), + ) + + +def about_content() -> rx.Component: + return menu_content( + rx.el.div( + rx.el.div( + solutions_item("Company", "Profile02Icon", "/about/"), + solutions_item("Careers", "WorkIcon", JOBS_BOARD_URL), + class_name="p-4 flex flex-col rounded-xl bg-white-1 h-full dark:shadow-none dark:border dark:border-m-slate-9 dark:bg-m-slate-11 shadow-card", + ), + class_name="w-[12.5rem]", + ), + ) + + +def navigation_menu() -> rx.Component: + return ui.navigation_menu.root( + ui.navigation_menu.list( + menu_trigger("Platform", platform_content()), + menu_trigger("Solutions", solutions_content()), + menu_trigger("Resources", resources_content()), + ui.navigation_menu.item( + rx.el.a( + marketing_button( + "Pricing", + size="sm", + variant="ghost", + native_button=False, + ), + to="/pricing", + ), + class_name="xl:flex hidden px-1", + custom_attrs={"role": "menuitem"}, + ), + ui.navigation_menu.item( + rx.el.a( + marketing_button( + "Docs", + size="sm", + variant="ghost", + ), + to="/docs", + ), + class_name="xl:flex hidden px-1", + custom_attrs={"role": "menuitem"}, + ), + menu_trigger("About", about_content()), + class_name="flex flex-row items-center m-0 h-full list-none", + custom_attrs={"role": "menubar"}, + ), + ui.navigation_menu.list( + ui.navigation_menu.item( + github(), + custom_attrs={"role": "menuitem"}, + ), + ui.navigation_menu.item( + rx.el.a( + marketing_button( + "Sign In", + ui.icon("Login01Icon", class_name="scale-x-[-1]"), + size="sm", + variant="outline", + native_button=False, + ), + to=REFLEX_BUILD_URL, + target="_blank", + ), + custom_attrs={"role": "menuitem"}, + ), + ui.navigation_menu.item( + demo_form_dialog( + trigger=marketing_button( + "Book a Demo", + size="sm", + variant="primary", + class_name=" whitespace-nowrap max-xl:hidden", + native_button=False, + ), + ), + unstyled=True, + class_name="xl:flex hidden", + custom_attrs={"role": "menuitem"}, + ), + ui.navigation_menu.item( + navbar_sidebar_button(), + class_name="xl:hidden flex", + unstyled=True, + custom_attrs={"role": "menuitem"}, + ), + class_name="flex flex-row lg:gap-4 gap-2 m-0 h-full list-none items-center", + custom_attrs={"role": "menubar"}, + ), + ui.navigation_menu.portal( + ui.navigation_menu.positioner( + ui.navigation_menu.popup( + ui.navigation_menu.viewport( + unstyled=True, + class_name="relative h-full w-full overflow-hidden rounded-[inherit]", + ), + unstyled=True, + class_name="relative h-[var(--popup-height)] w-[var(--popup-width)] origin-[var(--transform-origin)] rounded-xl bg-m-slate-1 dark:bg-m-slate-12 navbar-shadow transition-[opacity,transform,width,height,scale,translate] duration-150 ease-[cubic-bezier(0.22,1,0.36,1)] data-[ending-style]:ease-[ease] data-[ending-style]:scale-90 data-[ending-style]:opacity-0 data-[ending-style]:duration-150 data-[starting-style]:scale-90 data-[starting-style]:opacity-0", + ), + unstyled=True, + class_name="safari-nav-positioner box-border h-[var(--positioner-height)] w-[var(--positioner-width)] max-w-[var(--available-width)] transition-[top,left,right,bottom] duration-[0.35s] ease-[cubic-bezier(0.22,1,0.36,1)] data-[instant]:transition-none", + side_offset=30, + align="start", + align_offset=-20, + position_method="fixed", + ), + ), + unstyled=True, + class_name="group/navigation-menu relative flex w-full items-center h-full justify-between gap-6 mx-auto flex-row", + ) + + +@rx.memo +def marketing_navbar() -> rx.Component: + from shared.views.hosting_banner import hosting_banner + + return rx.el.div( + hosting_banner(), + rx.el.header( + logo(), + navigation_menu(), + class_name="w-full max-w-[71.5rem] h-[4.5rem] mx-auto flex flex-row items-center p-5 rounded-b-xl backdrop-blur-[16px] shadow-[0_-2px_2px_1px_rgba(0,0,0,0.02),0_1px_1px_0_rgba(0,0,0,0.08),0_4px_8px_0_rgba(0,0,0,0.03),0_0_0_1px_#FFF_inset] dark:shadow-none dark:border-x dark:border-b dark:border-m-slate-10 bg-gradient-to-b from-white to-m-slate-1 dark:from-m-slate-11 dark:to-m-slate-12", + ), + class_name="flex flex-col w-full top-0 z-[9999] fixed self-center", + ) diff --git a/shared/views/sidebar/__init__.py b/shared/views/sidebar/__init__.py new file mode 100644 index 0000000..e1f05b9 --- /dev/null +++ b/shared/views/sidebar/__init__.py @@ -0,0 +1,171 @@ +import reflex as rx +from reflex.style import toggle_color_mode + +import reflex_ui as ui +from reflex_ui import button +from shared.components.icons import get_icon +from shared.constants import DISCORD_URL, GITHUB_URL, TWITTER_URL +from shared.views.hosting_banner import HostingBannerState + + +def social_menu_item( + icon: str, + url: str = "/", + border: bool = False, +) -> rx.Component: + aria_labels = { + "github": "Visit Reflex on GitHub", + "twitter": "Follow Reflex on X", + "discord": "Join Reflex Discord community", + } + return rx.link( + get_icon(icon=icon, class_name="!text-slate-9"), + class_name="flex justify-center items-center gap-2 hover:bg-slate-3 px-4 py-[0.875rem] w-full h-[47px] transition-bg overflow-hidden" + + (" border-slate-4 border-x border-solid border-y-0" if border else ""), + href=url, + is_external=True, + custom_attrs={"aria-label": aria_labels.get(icon, f"Visit {icon}")}, + ) + + +def drawer_socials() -> rx.Component: + return rx.box( + social_menu_item("github", GITHUB_URL), + social_menu_item( + "twitter", + TWITTER_URL, + border=True, + ), + social_menu_item("discord", DISCORD_URL), + class_name="flex flex-row items-center border-slate-4 border-y-0 !border-b w-full", + ) + + +def drawer_item(text: str, url: str, active_str: str = "") -> rx.Component: + router_path = rx.State.router.page.path + if not url.endswith("/"): + url += "/" + active = router_path.contains(active_str) + if active_str == "docs": + active = rx.cond( + router_path.contains("hosting") + | router_path.contains("library") + | router_path.contains("gallery"), + False, + active, + ) + if active_str == "": + active = False + return rx.link( + text, + href=url, + underline="none", + color=rx.cond(active, "var(--c-violet-9)", "var(--c-slate-9)"), + class_name="flex justify-center items-center border-slate-4 px-4 py-[0.875rem] border-t-0 border-b border-solid w-full font-small hover:!text-violet-9 border-x-0", + ) + + +def navbar_sidebar_drawer(trigger: rx.Component) -> rx.Component: + return rx.drawer.root( + rx.drawer.trigger( + trigger, + ), + rx.drawer.portal( + rx.drawer.content( + rx.box( + drawer_item("Docs", "/docs", "docs"), + drawer_item("Templates", "/gallery", "gallery"), + drawer_item("Blog", "/blog", "blog"), + drawer_item("Case Studies", "/customers", "customers"), + drawer_item("Components", "/library", "library"), + drawer_item("Open Source", "/framework", "open-source"), + drawer_item("Cloud", "/cloud", "hosting"), + drawer_item("Pricing", "/pricing", "pricing"), + drawer_socials(), + rx.el.button( + rx.color_mode.icon( + light_component=rx.icon( + "sun", size=16, class_name="!text-slate-9" + ), + dark_component=rx.icon( + "moon", size=16, class_name="!text-slate-9" + ), + ), + on_click=toggle_color_mode, + class_name="flex flex-row justify-center items-center px-3 py-0.5 w-full h-[47px]", + custom_attrs={"aria-label": "Toggle color mode"}, + ), + class_name="flex flex-col items-center dark:bg-m-slate-12 bg-m-slate-1 w-full h-full", + ), + class_name=ui.cn( + "dark:!bg-m-slate-12 !bg-m-slate-1 w-full h-full !outline-none", + rx.cond( + HostingBannerState.is_banner_visible, + "!top-[137px]", + "!top-[77px]", + ), + ), + ) + ), + direction="bottom", + ) + + +def docs_sidebar_drawer(sidebar: rx.Component, trigger: rx.Component) -> rx.Component: + return rx.drawer.root( + rx.drawer.trigger(trigger, as_child=True), + rx.drawer.portal( + rx.drawer.overlay( + class_name="!bg-[rgba(0,0,0,0.1)] backdrop-blur-[4px]", + ), + rx.drawer.content( + rx.box( + rx.drawer.close( + rx.box( + class_name="absolute left-1/2 transform -translate-x-1/2 top-[-12px] flex-shrink-0 bg-slate-9 rounded-full w-[96px] h-[5px]", + ), + as_child=True, + ), + sidebar, + class_name="relative flex flex-col w-full", + ), + class_name="!top-[4rem] flex-col !bg-secondary-1 rounded-[24px_24px_0px_0px] w-full h-full !outline-none", + ), + ), + ) + + +def navbar_sidebar_button() -> rx.Component: + return rx.box( + navbar_sidebar_drawer( + button( + ui.icon( + "Menu01Icon", + style={ + "[data-state=open] &": { + "display": "none", + }, + "[data-state=closed] &": { + "display": "flex", + }, + }, + ), + ui.icon( + "Cancel01Icon", + style={ + "[data-state=open] &": { + "display": "flex", + }, + "[data-state=closed] &": { + "display": "none", + }, + }, + ), + size="icon-sm", + variant="outline", + custom_attrs={"aria-label": "Open sidebar"}, + native_button=False, + ), + ), + class_name="flex justify-center items-center size-8", + )