From 6a49bba81cf7e5c9ad18b2643cfa3e2121f8214e Mon Sep 17 00:00:00 2001 From: Ahmad Hakim Date: Mon, 27 Jul 2026 15:56:31 +0300 Subject: [PATCH] new: dashboards --- assets/social/dashboard.webp | Bin 0 -> 18344 bytes native/export.py | 13 + native/lib/blocks/bar_chart_02.py | 4 +- native/lib/blocks/kpi_card_01.py | 8 +- native/lib/dashboards/dashboard_01.py | 353 +++++++++++++++++++ native/lib/dashboards/dashboard_02.py | 279 +++++++++++++++ native/lib/dashboards/dashboard_03.py | 438 ++++++++++++++++++++++++ native/pages/blocks.py | 7 + native/pages/dashboard.py | 39 +++ native/registry/dashboard_introspect.py | 62 ++++ native/templates/_dashboard.py | 84 +++++ native/templates/_dashboard_example.py | 61 ++++ native/templates/navbar.py | 1 + native/templates/sublayout.py | 2 +- scripts/generate_preview_cards.py | 5 + 15 files changed, 1352 insertions(+), 4 deletions(-) create mode 100644 assets/social/dashboard.webp create mode 100644 native/lib/dashboards/dashboard_01.py create mode 100644 native/lib/dashboards/dashboard_02.py create mode 100644 native/lib/dashboards/dashboard_03.py create mode 100644 native/pages/dashboard.py create mode 100644 native/registry/dashboard_introspect.py create mode 100644 native/templates/_dashboard.py create mode 100644 native/templates/_dashboard_example.py diff --git a/assets/social/dashboard.webp b/assets/social/dashboard.webp new file mode 100644 index 0000000000000000000000000000000000000000..24dc8b03f86c39e676e9acce04fd30867729cf58 GIT binary patch literal 18344 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 literal 0 HcmV?d00001 diff --git a/native/export.py b/native/export.py index 9b57f7d..c587c4f 100644 --- a/native/export.py +++ b/native/export.py @@ -5,6 +5,7 @@ from native.pages.blocks import blocks_page from native.pages.components import components_page from native.pages.create import create_page +from native.pages.dashboard import dashboard_page from native.pages.docs import docs_page from native.pages.landing import landing_page from native.pages.typeset import typeset_page @@ -15,6 +16,18 @@ def export(app: App): + app.add_page( + component=dashboard_page(), + route="/dashboards", + title="Dashboards - buridan/native", + meta=generate_site_meta_tags( + title="Dashboards", + url="/dashboards", + description="Build responsive dashboards for analytics, monitoring, and data-driven applications using composable UI components.", + social_card="dashboards.webp", + ), + ) + app.add_page( component=blocks_page(), route="/blocks", diff --git a/native/lib/blocks/bar_chart_02.py b/native/lib/blocks/bar_chart_02.py index 16db58d..6443927 100644 --- a/native/lib/blocks/bar_chart_02.py +++ b/native/lib/blocks/bar_chart_02.py @@ -100,12 +100,12 @@ def bar_chart_02(): ), rx.el.p("$0.7M", class_name="mt-0.5 text-base font-semibold"), ), - class_name="flex gap-10 px-4", + class_name="flex gap-10", ), rx.el.div( rx.el.div(_chart(show_y_axis=False), class_name="sm:hidden"), rx.el.div(_chart(show_y_axis=True), class_name="hidden sm:block"), ), class_name=chart_tooltip_content([1, 2], "square") - + " w-full flex flex-col gap-6", + + " w-full flex flex-col gap-4", ) diff --git a/native/lib/blocks/kpi_card_01.py b/native/lib/blocks/kpi_card_01.py index 2d1b221..d5b3fa1 100644 --- a/native/lib/blocks/kpi_card_01.py +++ b/native/lib/blocks/kpi_card_01.py @@ -21,6 +21,12 @@ "change": "-4.8%", "color": "bg-chart-3", }, + { + "name": "Monthly retention", + "stat": "82.4%", + "change": "+2.7%", + "color": "bg-chart-4", + }, ] @@ -52,5 +58,5 @@ def _kpi_card(name: str, stat: str, change: str, color: str) -> rx.Component: def kpi_card_01(): return rx.el.dl( *[_kpi_card(**item) for item in data], - class_name="w-full grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3", + class_name="w-full grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-4", ) diff --git a/native/lib/dashboards/dashboard_01.py b/native/lib/dashboards/dashboard_01.py new file mode 100644 index 0000000..e2544e3 --- /dev/null +++ b/native/lib/dashboards/dashboard_01.py @@ -0,0 +1,353 @@ +import reflex as rx + +from components.core.hugeicon import hi +from components.ui.avatar import avatar +from components.ui.badge import badge +from components.ui.checkbox import checkbox +from components.ui.table import table +from native.lib.blocks.bar_chart_02 import bar_chart_02 +from native.lib.blocks.kpi_card_01 import kpi_card_01 + +NAV_ITEMS = [ + {"label": "Overview", "icon": "DashboardSquare01Icon"}, + {"label": "Analytics", "icon": "ChartBarLineIcon"}, + {"label": "Projects", "icon": "FolderIcon"}, + {"label": "Team", "icon": "UserGroupIcon"}, + {"label": "Settings", "icon": "Settings01Icon"}, +] + +INVOICES = [ + { + "id": "INV-0041", + "client": "Miriam Okafor", + "initials": "MO", + "avatar": "https://i.pravatar.cc/80?img=1", + "project": "Brand Refresh", + "amount": 4200.00, + "method": "Wire Transfer", + "due": "Jun 30, 2026", + "due_date": "2026-06-30", + "status": "Pending", + }, + { + "id": "INV-0040", + "client": "Theo Hartmann", + "initials": "TH", + "avatar": "https://i.pravatar.cc/80?img=12", + "project": "API Integration", + "amount": 1850.00, + "method": "Credit Card", + "due": "Jun 15, 2026", + "due_date": "2026-06-15", + "status": "Paid", + }, + { + "id": "INV-0039", + "client": "Suki Nakamura", + "initials": "SN", + "avatar": "https://i.pravatar.cc/80?img=5", + "project": "Dashboard UI", + "amount": 6500.00, + "method": "ACH", + "due": "Jun 01, 2026", + "due_date": "2026-06-01", + "status": "Overdue", + }, + { + "id": "INV-0038", + "client": "Elias Ferreira", + "initials": "EF", + "avatar": "https://i.pravatar.cc/80?img=3", + "project": "Mobile App MVP", + "amount": 9000.00, + "method": "Wire Transfer", + "due": "May 28, 2026", + "due_date": "2026-05-28", + "status": "Paid", + }, + { + "id": "INV-0037", + "client": "Priya Menon", + "initials": "PM", + "avatar": "https://i.pravatar.cc/80?img=9", + "project": "SEO Audit", + "amount": 780.00, + "method": "Credit Card", + "due": "May 10, 2026", + "due_date": "2026-05-10", + "status": "Refunded", + }, + { + "id": "INV-0036", + "client": "Dmitri Volkov", + "initials": "DV", + "avatar": "https://i.pravatar.cc/80?img=11", + "project": "Data Pipeline", + "amount": 3350.00, + "method": "ACH", + "due": "Apr 25, 2026", + "due_date": "2026-04-25", + "status": "Paid", + }, + { + "id": "INV-0035", + "client": "Amara Diallo", + "initials": "AD", + "avatar": "https://i.pravatar.cc/80?img=16", + "project": "Design System", + "amount": 5400.00, + "method": "Wire Transfer", + "due": "Jun 22, 2026", + "due_date": "2026-06-22", + "status": "Pending", + }, + { + "id": "INV-0034", + "client": "Noah Bergström", + "initials": "NB", + "avatar": "https://i.pravatar.cc/80?img=14", + "project": "Marketing Site", + "amount": 2100.00, + "method": "Credit Card", + "due": "Apr 18, 2026", + "due_date": "2026-04-18", + "status": "Paid", + }, + { + "id": "INV-0033", + "client": "Lucia Romano", + "initials": "LR", + "avatar": "https://i.pravatar.cc/80?img=20", + "project": "Onboarding Flow", + "amount": 3950.00, + "method": "ACH", + "due": "May 31, 2026", + "due_date": "2026-05-31", + "status": "Overdue", + }, + { + "id": "INV-0032", + "client": "Kwame Mensah", + "initials": "KM", + "avatar": "https://i.pravatar.cc/80?img=15", + "project": "Analytics Setup", + "amount": 1280.00, + "method": "Credit Card", + "due": "Apr 09, 2026", + "due_date": "2026-04-09", + "status": "Paid", + }, +] + +STATUS_CONFIG = { + "Paid": {"variant": "default", "dot": "bg-primary-foreground"}, + "Pending": {"variant": "secondary", "dot": "bg-muted-foreground"}, + "Overdue": {"variant": "destructive", "dot": "bg-destructive"}, + "Refunded": {"variant": "outline", "dot": "bg-muted-foreground"}, +} + + +def main_kpi_section(): + return kpi_card_01() + + +def main_chart_section(): + return rx.el.div( + bar_chart_02(), class_name="w-full border border-input/90 rounded-2xl p-4" + ) + + +def render_status_badge(status: str) -> rx.Component: + config = STATUS_CONFIG.get( + status, {"variant": "default", "dot": "bg-muted-foreground"} + ) + + return badge( + rx.el.span( + class_name=f"inline-block size-1.5 shrink-0 rounded-lg {config['dot']}" + ), + status, + variant=config["variant"], + class_name="gap-1.5 text-[11px] font-medium", + ) + + +def render_row(inv: dict) -> rx.Component: + formatted_amount = f"{inv['amount']:,.2f}" + + return table.row( + table.cell( + checkbox.root( + checkbox.indicator(), + value=inv["id"], + **{"data-dt-select": "true"}, + ), + class_name="pl-4 py-2", + ), + table.cell( + rx.el.span( + inv["id"], + class_name="font-mono text-xs text-muted-foreground py-2", + ) + ), + table.cell( + rx.el.div( + avatar.root( + avatar.image( + src=inv["avatar"], + class_name="shrink-0 border border-border grayscale", + ), + ), + rx.el.span( + inv["client"], + class_name="truncate text-sm font-medium text-foreground", + ), + class_name="flex min-w-0 items-center gap-2.5", + ), + sort_value=inv["client"], + class_name="py-2", + ), + table.cell( + rx.el.span( + inv["project"], + class_name="block max-w-[140px] truncate text-sm text-muted-foreground", + ), + class_name="hidden sm:table-cell py-2", + ), + table.cell( + rx.el.span(inv["method"], class_name="text-sm text-muted-foreground"), + class_name="hidden md:table-cell py-2", + ), + table.cell( + rx.el.span( + inv["due"], + class_name="text-sm text-muted-foreground tabular-nums", + ), + sort_value=inv["due_date"], + class_name="hidden md:table-cell py-2", + ), + table.cell(render_status_badge(inv["status"])), + table.cell( + rx.el.span( + f"${formatted_amount}", + class_name="block text-right text-sm font-semibold text-foreground tabular-nums", + ), + sort_value=inv["amount"], + class_name="py-2", + ), + class_name="whitespace-nowrap !py-2", + ) + + +def main_table_section(): + + return rx.el.div( + table.search( + for_table="invoices-table", + class_name="mb-4 w-full max-w-sm", + ), + table.root( + table.header( + table.row( + table.head( + checkbox.root( + checkbox.indicator(), + **{"data-dt-select-all": "true"}, + ), + class_name="w-10 pl-4", + ), + table.head("Invoice"), + table.head("Client", sort_key="client"), + table.head("Project", class_name="hidden sm:table-cell"), + table.head("Method", class_name="hidden md:table-cell"), + table.head( + "Due", + sort_key="due", + class_name="hidden md:table-cell", + ), + table.head("Status"), + table.head( + "Amount", + sort_key="amount", + class_name="text-right", + ), + ) + ), + table.body(*[render_row(inv) for inv in INVOICES]), + id="invoices-table", + paginate=True, + page_size=7, + class_name="w-full", + ), + class_name="w-full", + ) + + +def main(): + return rx.el.main( + rx.el.h1("Overview", class_name="text-2xl font-semibold"), + main_kpi_section(), + main_chart_section(), + main_table_section(), + class_name="w-full flex-1 h-full border border-input/90 overflow-y-auto p-6 flex flex-col gap-6", + ) + + +def sidebar_header(): + return rx.el.div( + rx.el.span("Acme", class_name="font-semibold text-lg"), + class_name="flex items-center gap-2 px-4 h-10 shrink-0 border-b border-input/90", + ) + + +def _sidebar_item(label: str, icon: str) -> rx.Component: + return rx.el.div( + hi(icon, class_name="size-4 shrink-0"), + rx.el.span(label, class_name="text-sm"), + class_name="flex items-center gap-2.5 px-4 py-2 rounded-lg cursor-pointer", + ) + + +def sidebar_items(): + return rx.el.div( + *[_sidebar_item(i["label"], i["icon"]) for i in NAV_ITEMS], + class_name="flex flex-col gap-1 py-2", + ) + + +def sidebar_footer(): + return rx.el.div( + rx.el.div( + "AC", + class_name="size-8 rounded-full bg-muted border border-input/90 flex items-center justify-center text-xs font-medium shrink-0", + ), + rx.el.div( + rx.el.p( + "Avery Cole", class_name="text-sm font-medium leading-tight truncate" + ), + rx.el.p( + "avery@acme.com", + class_name="text-xs text-muted-foreground leading-tight truncate", + ), + class_name="min-w-0", + ), + class_name="flex items-center gap-2.5 px-4 py-3 border-t border-input/90 shrink-0", + ) + + +def sidebar(): + return rx.el.aside( + sidebar_header(), + sidebar_items(), + rx.el.div(class_name="flex-1"), + sidebar_footer(), + class_name="w-full max-w-64 h-full bg-accent dark:bg-card border-y border-l border-input/90 hidden md:flex md:flex-col", + ) + + +def dashboard_01(): + return rx.el.div( + sidebar(), + main(), + class_name="w-full h-screen flex", + ) diff --git a/native/lib/dashboards/dashboard_02.py b/native/lib/dashboards/dashboard_02.py new file mode 100644 index 0000000..f7b1599 --- /dev/null +++ b/native/lib/dashboards/dashboard_02.py @@ -0,0 +1,279 @@ +import reflex as rx + +from components.core.hugeicon import hi +from components.ui.avatar import avatar +from components.ui.badge import badge +from native.lib.charts.area.v8 import area_chart_with_gradient + +NAV_ITEMS = [ + {"label": "Overview", "icon": "DashboardSquare01Icon", "active": True}, + {"label": "Analytics", "icon": "ChartBarLineIcon", "active": False}, + {"label": "Projects", "icon": "FolderIcon", "active": False}, + {"label": "Team", "icon": "UserGroupIcon", "active": False}, + {"label": "Settings", "icon": "Settings01Icon", "active": False}, +] + +STATS = [ + {"label": "Total revenue", "value": "$48.2k", "delta": "+12.4%"}, + {"label": "Active users", "value": "2,840", "delta": "+5.1%"}, + {"label": "Conversion", "value": "3.6%", "delta": "-0.8%"}, +] + +ACTIVITY = [ + { + "name": "Priya Nair", + "avatar": "https://i.pravatar.cc/150?img=45", + "icon": "RocketIcon", + "action": "deployed", + "target": "acme-web v2.4", + "time": "2 min ago", + }, + { + "name": "Marco Diaz", + "avatar": "https://i.pravatar.cc/150?img=33", + "icon": "GitMergeIcon", + "action": "merged", + "target": "PR #482: checkout refactor", + "time": "1 hour ago", + }, + { + "name": "Lena Fischer", + "avatar": "https://i.pravatar.cc/150?img=47", + "icon": "CheckmarkCircle01Icon", + "action": "closed", + "target": "8 tasks in Sprint 7", + "time": "3 hours ago", + }, +] + + +def sidebar_header(): + return rx.el.div( + hi("DashboardSquare01Icon", class_name="size-5 shrink-0"), + rx.el.span("Acme", class_name="font-semibold text-base"), + class_name="flex items-center gap-2 px-4 h-14 shrink-0 border-b border-input/90", + ) + + +def _sidebar_item(label: str, icon: str, active: bool = False) -> rx.Component: + return rx.el.div( + hi(icon, class_name="size-4 shrink-0"), + rx.el.span(label, class_name="text-sm"), + class_name=( + "flex items-center gap-2.5 px-3 py-2 rounded-lg cursor-pointer " + + ( + "bg-accent text-accent-foreground" + if active + else "text-muted-foreground hover:bg-accent/60 hover:text-accent-foreground" + ) + ), + ) + + +def sidebar_items(): + return rx.el.div( + *[_sidebar_item(i["label"], i["icon"], i["active"]) for i in NAV_ITEMS], + class_name="flex flex-col gap-1 p-2", + ) + + +def sidebar_footer(): + return rx.el.div( + avatar.root( + avatar.image( + src="https://i.pravatar.cc/150?img=15", + class_name="shrink-0 border border-input/90 grayscale", + ), + avatar.fallback("AC"), + class_name="size-8", + ), + rx.el.div( + rx.el.p( + "Avery Cole", class_name="text-sm font-medium leading-tight truncate" + ), + rx.el.p( + "avery@acme.com", + class_name="text-xs text-muted-foreground leading-tight truncate", + ), + class_name="min-w-0", + ), + class_name="flex items-center gap-2.5 px-4 py-3 border-t border-input/90 shrink-0", + ) + + +def sidebar(): + return rx.el.aside( + sidebar_header(), + sidebar_items(), + rx.el.div(class_name="flex-1"), + sidebar_footer(), + class_name="w-full max-w-64 h-full bg-accent dark:bg-card border-y border-l border-input/90 hidden md:flex md:flex-col", + ) + + +def navbar(): + return rx.el.div( + rx.el.div( + hi("Search01Icon", class_name="size-4 shrink-0 text-muted-foreground"), + rx.el.span( + "Search projects, people...", + class_name="text-sm text-muted-foreground", + ), + class_name=( + "flex items-center gap-2 h-9 w-full max-w-sm border border-input/90 " + "rounded-lg px-3 bg-background cursor-pointer" + ), + ), + rx.el.div( + rx.el.button( + hi("Notification03Icon", class_name="size-5"), + rx.el.span( + "2", + class_name=( + "absolute -top-1 -right-1 flex size-4 items-center justify-center " + "rounded-full bg-primary text-[9px] font-bold text-primary-foreground" + ), + ), + class_name="relative size-9 flex items-center justify-center rounded-lg hover:bg-accent", + ), + avatar.root( + avatar.image( + src="https://i.pravatar.cc/150?img=15", + class_name="shrink-0 border border-input/90 grayscale", + ), + avatar.fallback("AC"), + class_name="size-8 cursor-pointer", + ), + class_name="flex items-center gap-2", + ), + class_name=( + "flex items-center justify-between gap-3 h-14 shrink-0 px-4 sm:px-6 " + "border-b border-input/90 bg-background" + ), + ) + + +def _stat_card(label: str, value: str, delta: str) -> rx.Component: + is_negative = delta.startswith("-") + return rx.el.div( + rx.el.div( + rx.el.p(label, class_name="text-sm font-medium text-muted-foreground"), + badge( + delta, + variant="destructive" if is_negative else "secondary", + class_name="text-[11px]", + ), + class_name="flex items-center justify-between", + ), + rx.el.p(value, class_name="text-3xl font-semibold tracking-tight mt-3"), + class_name="flex-1 border border-input/90 rounded-2xl p-4", + ) + + +def main_stats_section(): + return rx.el.div( + *[_stat_card(s["label"], s["value"], s["delta"]) for s in STATS], + class_name="grid gap-4 sm:grid-cols-2 md:grid-cols-3", + ) + + +def main_chart_section(): + return rx.el.div( + rx.el.div( + rx.el.p("Performance", class_name="font-medium"), + badge("+18.2% MoM", variant="secondary", class_name="text-[11px]"), + class_name="flex items-center justify-between mb-2", + ), + area_chart_with_gradient(), + class_name="md:col-span-2 border border-input/90 rounded-2xl p-4", + ) + + +def _activity_row(item: dict) -> rx.Component: + return rx.el.div( + rx.el.div( + avatar.root( + avatar.image(src=item["avatar"], class_name="shrink-0 grayscale"), + avatar.fallback(item["name"][:2].upper()), + class_name="size-8", + ), + rx.el.div( + hi(item["icon"], class_name="size-2.5 text-muted-foreground"), + class_name=( + "absolute -right-1 -bottom-1 flex size-4 items-center justify-center " + "rounded-full border border-input/90 bg-background" + ), + ), + class_name="relative shrink-0", + ), + rx.el.div( + rx.el.p( + rx.el.span(item["name"], class_name="font-medium text-foreground"), + " ", + rx.el.span(item["action"], class_name="text-muted-foreground"), + " ", + rx.el.span(item["target"], class_name="font-medium text-foreground"), + class_name="text-sm leading-snug", + ), + rx.el.p(item["time"], class_name="text-xs text-muted-foreground mt-0.5"), + class_name="min-w-0 flex-1", + ), + class_name="flex items-start gap-3", + ) + + +def main_activity_section(): + return rx.el.div( + rx.el.p("Recent Activity", class_name="font-medium mb-4"), + rx.el.div( + *[_activity_row(item) for item in ACTIVITY], + class_name="flex flex-col gap-4", + ), + class_name="border border-input/90 rounded-2xl p-4", + ) + + +def main_performance_section(): + return rx.el.div( + main_chart_section(), + main_activity_section(), + class_name="grid gap-4 md:grid-cols-3", + ) + + +def main(): + return rx.el.main( + navbar(), + rx.el.div( + rx.el.div( + rx.el.div( + rx.el.h1( + "Overview", class_name="text-2xl font-semibold tracking-tight" + ), + rx.el.p( + "Here is what is happening across Acme today.", + class_name="text-sm text-muted-foreground", + ), + ), + badge( + rx.el.span(class_name="size-1.5 rounded-full bg-primary"), + "Live", + variant="secondary", + class_name="gap-1.5", + ), + class_name="flex flex-wrap items-end justify-between gap-3", + ), + main_stats_section(), + main_performance_section(), + class_name="flex flex-col gap-4 p-4 sm:p-6", + ), + class_name="w-full flex-1 h-full border border-input/90 overflow-y-auto flex flex-col", + ) + + +def dashboard_02(): + return rx.el.div( + sidebar(), + main(), + class_name="w-full h-screen flex", + ) diff --git a/native/lib/dashboards/dashboard_03.py b/native/lib/dashboards/dashboard_03.py new file mode 100644 index 0000000..8dff9fb --- /dev/null +++ b/native/lib/dashboards/dashboard_03.py @@ -0,0 +1,438 @@ +import reflex as rx + +from components.core.hugeicon import hi +from components.ui.avatar import avatar +from components.ui.button import button +from native.lib.charts.area.v8 import area_chart_with_gradient + +NAV_ITEMS = [ + {"label": "Dashboard", "icon": "DashboardSquare01Icon", "active": True}, + {"label": "Projects", "icon": "Folder01Icon", "active": False}, + {"label": "Analytics", "icon": "ChartBarLineIcon", "active": False}, + {"label": "Team", "icon": "UserGroupIcon", "active": False}, + {"label": "Reports", "icon": "GridViewIcon", "active": False}, +] + +ACTIVE_PROJECTS = [ + "Design System", + "API Integration", + "Mobile App", + "Analytics Rebuild", +] + +BREADCRUMB_ITEMS = [ + {"label": "Acme Inc", "icon": "SquareIcon"}, + {"label": "Marketing Site", "icon": None}, + {"label": "Preview", "icon": None}, +] + +STATS = [ + {"label": "Monthly Revenue", "value": "$48,240", "delta": "+12.5%"}, + {"label": "Active Users", "value": "8,941", "delta": "+4.3%"}, + {"label": "Conversion Rate", "value": "3.24%", "delta": "-0.8%"}, +] + +ACTIVITY = [ + { + "name": "Priya Nair", + "avatar": "https://i.pravatar.cc/150?img=45", + "action": "deployed", + "target": "web-app v2.4.0", + "time": "9:42 AM", + }, + { + "name": "Jonas Weber", + "avatar": "https://i.pravatar.cc/150?img=33", + "action": "merged", + "target": "PR 482 Checkout Refactor", + "time": "9:06 AM", + }, + { + "name": "Lena Fischer", + "avatar": "https://i.pravatar.cc/150?img=47", + "action": "closed", + "target": "8 tasks in Sprint 7", + "time": "8:31 AM", + }, + { + "name": "Diego Alvarez", + "avatar": "https://i.pravatar.cc/150?img=11", + "action": "invited", + "target": "Sofia Rossi to the team", + "time": "Yesterday", + }, +] + + +def sidebar_header(): + return rx.el.div( + rx.el.div( + hi("DashboardSquare01Icon", class_name="size-5 shrink-0"), + rx.el.span("Acme", class_name="font-semibold text-base"), + class_name="flex items-center gap-2", + ), + rx.el.div( + hi("SparklesIcon", class_name="size-3 shrink-0"), + rx.el.span("Pro", class_name="text-xs font-medium"), + class_name="flex items-center gap-1 bg-foreground text-background rounded-full px-2.5 py-1", + ), + class_name="flex items-center justify-between px-4 h-14 shrink-0", + ) + + +def new_report_button(): + return button( + hi("PlusSignIcon", class_name="size-4"), + rx.el.span("New Report", class_name="text-sm font-medium"), + class_name="w-full", + ) + + +def _section_label(text: str) -> rx.Component: + return rx.el.p( + text, + class_name="px-4 pt-4 pb-1 text-xs font-medium text-muted-foreground", + ) + + +def _sidebar_item(label: str, icon: str, active: bool = False) -> rx.Component: + return rx.el.div( + hi(icon, class_name="size-4 shrink-0"), + rx.el.span(label, class_name="text-sm font-medium"), + class_name=( + "flex items-center gap-2.5 mx-2 px-3 py-2 rounded-lg cursor-pointer " + + ( + "bg-accent text-accent-foreground" + if active + else "text-muted-foreground hover:bg-accent/60 hover:text-accent-foreground" + ) + ), + ) + + +def sidebar_platform_items(): + return rx.el.div( + _section_label("Platform"), + *[_sidebar_item(i["label"], i["icon"], i["active"]) for i in NAV_ITEMS], + class_name="flex flex-col", + ) + + +def _project_item(label: str) -> rx.Component: + return rx.el.div( + hi("Loading03Icon", class_name="size-4 shrink-0 text-muted-foreground"), + rx.el.span(label, class_name="text-sm"), + class_name="flex items-center gap-2.5 mx-2 px-3 py-2 rounded-lg text-muted-foreground hover:bg-accent/60 cursor-pointer", + ) + + +def sidebar_active_projects(): + return rx.el.div( + _section_label("Active Projects"), + *[_project_item(p) for p in ACTIVE_PROJECTS], + class_name="flex flex-col", + ) + + +def sidebar_usage_card(): + return rx.el.div( + rx.el.div( + rx.el.div( + hi("SparklesIcon", class_name="size-3.5"), + rx.el.span("Monthly Usage", class_name="text-sm font-medium"), + class_name="flex items-center gap-1.5", + ), + rx.el.span("Resets May 1", class_name="text-xs text-background/60"), + class_name="flex items-center justify-between", + ), + rx.el.p( + "You have used 82% of your plan this cycle. Upgrade for unlimited requests.", + class_name="text-xs text-background/70 mt-2 leading-relaxed", + ), + rx.el.div( + rx.el.div(class_name="h-1.5 rounded-full bg-background w-[82%]"), + class_name="h-1.5 w-full rounded-full bg-background/20 mt-3", + ), + rx.el.div( + rx.el.span("82% Used", class_name="text-xs font-medium"), + rx.el.span("18% Left", class_name="text-xs text-background/60"), + class_name="flex items-center justify-between mt-1.5", + ), + rx.el.button( + hi("Notification03Icon", class_name="size-3.5"), + rx.el.span("Upgrade Plan", class_name="text-sm font-medium"), + class_name="flex items-center justify-center gap-2 h-9 w-full rounded-lg bg-background text-foreground mt-3", + ), + class_name="mx-3 mt-4 p-4 rounded-xl bg-foreground text-background", + ) + + +def sidebar_footer(): + return rx.el.div( + avatar.root( + avatar.image( + src="https://i.pravatar.cc/150?img=47", + class_name="shrink-0 grayscale", + ), + avatar.fallback("MC"), + class_name="size-8", + ), + rx.el.div( + rx.el.p( + "Maya Chen", class_name="text-sm font-medium leading-tight truncate" + ), + rx.el.p( + "maya@acme.com", + class_name="text-xs text-muted-foreground leading-tight truncate", + ), + class_name="min-w-0 flex-1", + ), + hi("UnfoldMoreIcon", class_name="size-4 shrink-0 text-muted-foreground"), + class_name="flex items-center gap-2.5 px-4 py-3 border-t border-input/90 shrink-0", + ) + + +def sidebar(): + return rx.el.aside( + sidebar_header(), + rx.el.div(new_report_button(), class_name="p-2 w-full"), + rx.el.div( + sidebar_platform_items(), + sidebar_active_projects(), + class_name="flex-1 overflow-y-auto py-3", + ), + sidebar_usage_card(), + sidebar_footer(), + class_name="w-full max-w-64 h-full bg-accent dark:bg-card border-y border-l border-input/90 hidden md:flex md:flex-col", + ) + + +def _breadcrumb_item(label: str, icon: str | None, is_last: bool) -> rx.Component: + return rx.el.div( + hi(icon, class_name="size-3.5 shrink-0") if icon else rx.el.div(), + rx.el.span( + label, + class_name=( + "text-sm " + ("font-semibold" if is_last else "text-muted-foreground") + ), + ), + hi("ArrowDown01Icon", class_name="size-3.5 shrink-0 text-muted-foreground"), + class_name="flex items-center gap-1.5", + ) + + +def breadcrumb(): + items = [] + for i, crumb in enumerate(BREADCRUMB_ITEMS): + items.append( + _breadcrumb_item( + crumb["label"], crumb["icon"], i == len(BREADCRUMB_ITEMS) - 1 + ) + ) + if i < len(BREADCRUMB_ITEMS) - 1: + items.append(rx.el.span("/", class_name="text-muted-foreground/50")) + return rx.el.div(*items, class_name="flex items-center gap-2.5") + + +def topbar_search(): + return rx.el.div( + hi("Search01Icon", class_name="size-4 shrink-0 text-muted-foreground"), + rx.el.span("Search...", class_name="text-sm text-muted-foreground flex-1"), + rx.el.kbd( + "⌘K", + class_name="text-[10px] font-medium text-muted-foreground bg-muted rounded px-1.5 py-0.5", + ), + class_name="hidden lg:flex items-center gap-2 h-9 w-64 rounded-lg border border-input/90 px-3 bg-background", + ) + + +def topbar_status_pill(): + return rx.el.div( + rx.el.span(class_name="size-1.5 rounded-full bg-orange-500"), + rx.el.span("Degraded", class_name="text-xs font-medium"), + class_name="hidden sm:flex items-center gap-1.5 h-9 rounded-lg border border-input/90 px-3", + ) + + +def topbar(): + return rx.el.div( + rx.el.div( + hi( + "SidebarLeftIcon", + class_name="size-4 shrink-0 text-muted-foreground hidden md:block", + ), + class_name="flex items-center gap-4 min-w-0", + ), + rx.el.div( + topbar_search(), + topbar_status_pill(), + rx.el.button( + hi("Notification03Icon", class_name="size-4.5"), + rx.el.span( + class_name="absolute top-1.5 right-1.5 size-1.5 rounded-full bg-foreground" + ), + class_name="relative size-9 flex items-center justify-center rounded-lg hover:bg-accent shrink-0", + ), + avatar.root( + avatar.image( + src="https://i.pravatar.cc/150?img=47", + class_name="shrink-0 grayscale", + ), + avatar.fallback("MC"), + class_name="size-8 shrink-0 cursor-pointer", + ), + class_name="flex items-center gap-3 shrink-0", + ), + class_name="flex items-center justify-between gap-4 h-14 shrink-0 px-4 sm:px-6 border-b border-input/90 bg-background", + ) + + +def _stat_card(label: str, value: str, delta: str) -> rx.Component: + is_negative = delta.startswith("-") + return rx.el.div( + rx.el.div( + rx.el.p(label, class_name="text-sm text-muted-foreground"), + rx.el.div( + hi( + "ArrowDownRight01Icon" if is_negative else "ArrowUpRight01Icon", + class_name="size-3.5", + ), + rx.el.span(delta, class_name="text-sm font-medium"), + class_name=( + "flex items-center gap-1 " + + ("text-muted-foreground" if is_negative else "text-emerald-600") + ), + ), + class_name="flex items-center justify-between", + ), + rx.el.p(value, class_name="text-3xl font-bold tracking-tight mt-3"), + rx.el.p("Vs. Last Month", class_name="text-xs text-muted-foreground mt-2"), + class_name="flex-1 border border-input/90 rounded-2xl p-5", + ) + + +def main_stats_section(): + return rx.el.div( + *[_stat_card(s["label"], s["value"], s["delta"]) for s in STATS], + class_name="grid gap-4 sm:grid-cols-2 md:grid-cols-3", + ) + + +def main_chart_section(): + return rx.el.div( + rx.el.div( + rx.el.div( + rx.el.p("Revenue", class_name="font-semibold"), + rx.el.p("Last 8 Months", class_name="text-sm text-muted-foreground"), + ), + rx.el.span( + "+18.2% YTD", + class_name="text-xs font-medium bg-muted rounded-lg px-2.5 py-1.5 h-fit", + ), + class_name="flex items-start justify-between mb-4", + ), + area_chart_with_gradient(), + class_name="lg:col-span-2 border border-input/90 rounded-2xl p-5", + ) + + +def _activity_row(item: dict) -> rx.Component: + return rx.el.div( + avatar.root( + avatar.image(src=item["avatar"], class_name="shrink-0 grayscale"), + avatar.fallback(item["name"][:2].upper()), + class_name="size-9", + ), + rx.el.div( + rx.el.p( + rx.el.span(item["name"], class_name="font-semibold text-foreground"), + " ", + rx.el.span(item["action"], class_name="text-muted-foreground"), + " ", + rx.el.span(item["target"], class_name="font-semibold text-foreground"), + class_name="text-sm leading-snug", + ), + rx.el.p(item["time"], class_name="text-xs text-muted-foreground mt-1"), + class_name="min-w-0 flex-1", + ), + class_name="flex items-start gap-3 py-3 border-b border-input/60 last:border-0", + ) + + +def main_activity_section(): + return rx.el.div( + rx.el.div( + rx.el.p("Recent Activity", class_name="font-semibold"), + rx.el.span( + "View All", + class_name="text-sm text-muted-foreground cursor-pointer hover:text-foreground", + ), + class_name="flex items-center justify-between mb-1", + ), + rx.el.div( + *[_activity_row(item) for item in ACTIVITY], + class_name="flex flex-col", + ), + class_name="border border-input/90 rounded-2xl p-5", + ) + + +def main_performance_section(): + return rx.el.div( + main_chart_section(), + main_activity_section(), + class_name="grid gap-4 lg:grid-cols-3", + ) + + +def main_active_projects_header(): + return rx.el.div( + rx.el.p("Active Projects", class_name="font-semibold"), + rx.el.span( + "View All", + class_name="text-sm text-muted-foreground cursor-pointer hover:text-foreground", + ), + class_name="flex items-center justify-between p-5 border border-input/90 rounded-2xl", + ) + + +def main_header_row(): + return rx.el.div( + rx.el.div( + rx.el.h1( + "Welcome Back, Maya", class_name="text-3xl font-bold tracking-tight" + ), + rx.el.p( + "Here is what is happening across your workspace today.", + class_name="text-sm text-muted-foreground mt-1", + ), + ), + rx.el.button( + hi("ArrowUpRight01Icon", class_name="size-4"), + rx.el.span("Export Report", class_name="text-sm font-medium"), + class_name="flex items-center gap-2 h-9 rounded-lg border border-input/90 px-3 shrink-0", + ), + class_name="flex flex-wrap items-end justify-between gap-3", + ) + + +def main(): + return rx.el.main( + topbar(), + rx.el.div( + main_header_row(), + main_stats_section(), + main_performance_section(), + main_active_projects_header(), + class_name="flex flex-col gap-4 p-4 sm:p-6", + ), + class_name="w-full flex-1 h-full border border-input/90 overflow-y-auto flex flex-col", + ) + + +def dashboard_03(): + return rx.el.div( + sidebar(), + main(), + class_name="w-full h-screen flex", + ) diff --git a/native/pages/blocks.py b/native/pages/blocks.py index aadb4b1..fe89e90 100644 --- a/native/pages/blocks.py +++ b/native/pages/blocks.py @@ -127,4 +127,11 @@ def blocks_page(): for prefix, title, description in BLOCK_GROUPS ], class_name="flex flex-col gap-8", + on_mount=rx.call_script( + """ + requestAnimationFrame(() => { + Prism.highlightAll(); + }); + """ + ), ) diff --git a/native/pages/dashboard.py b/native/pages/dashboard.py new file mode 100644 index 0000000..789919c --- /dev/null +++ b/native/pages/dashboard.py @@ -0,0 +1,39 @@ +from reflex.event import call_script +from reflex_components_core.el import div + +from native.templates._dashboard_example import dashboard_example +from native.templates.sublayout import sub_layout_decorator + + +@sub_layout_decorator( + badge_name="Dashboards", + title="Dashboards for Data Apps", + description="Build responsive dashboards for analytics, monitoring, and data-driven applications using composable UI components.", + search=div(), +) +def dashboard_page(): + return div( + dashboard_example( + "dashboard_01", + title="Dashboard 01", + description="A KPI row, bar chart, and data table for a general-purpose analytics dashboard.", + ), + dashboard_example( + "dashboard_02", + title="Dashboard 02", + description="Dashboard with search top bar, avatar, and dashboard placeholder cards.", + ), + dashboard_example( + "dashboard_03", + title="Dashboard 03", + description="A complex dashboard with intricate sidebar, menu items, call-to-action card, analytics, and a menu top bar.", + ), + class_name="flex flex-col gap-16", + on_mount=call_script( + """ + requestAnimationFrame(() => { + Prism.highlightAll(); + }); + """ + ), + ) diff --git a/native/registry/dashboard_introspect.py b/native/registry/dashboard_introspect.py new file mode 100644 index 0000000..3738242 --- /dev/null +++ b/native/registry/dashboard_introspect.py @@ -0,0 +1,62 @@ +import ast +import importlib +from pathlib import Path + +DASHBOARDS_DIR = Path("native/lib/dashboards") + +DEPENDENCY_BUCKETS = { + "components.ui.": "components", + "components.core.": "components", + "native.lib.blocks.": "blocks", +} + + +def _classify_import(module_path: str) -> str | None: + for prefix, bucket in DEPENDENCY_BUCKETS.items(): + if module_path.startswith(prefix): + return bucket + return None # e.g. "reflex", "reflex_components_core.el" — not tracked + + +def _extract_dependencies(source: str) -> dict[str, list[str]]: + """Walks `from X import a, b` statements and buckets the imported + names (not the module path) by what X starts with. So: + from components.ui.avatar import avatar -> components: ["avatar"] + from native.lib.blocks.kpi_card_01 import kpi_card_01 + -> blocks: ["kpi_card_01"] + """ + tree = ast.parse(source) + deps: dict[str, list[str]] = {"components": [], "blocks": []} + for node in ast.walk(tree): + if isinstance(node, ast.ImportFrom) and node.module: + bucket = _classify_import(node.module) + if bucket is None: + continue + for alias in node.names: + name = alias.asname or alias.name + if name not in deps[bucket]: + deps[bucket].append(name) + return deps + + +def list_dashboard_ids() -> list[str]: + """e.g. ["dashboard_01", "dashboard_02", ...] from files on disk.""" + return sorted(p.stem for p in DASHBOARDS_DIR.glob("dashboard_*.py")) + + +def load_dashboard_example(dashboard_id: str) -> dict: + """dashboard_id="dashboard_01" -> reads + native/lib/dashboards/dashboard_01.py, imports it, and grabs the + top-level dashboard_01() function (same-name convention you're using).""" + file_path = DASHBOARDS_DIR / f"{dashboard_id}.py" + source = file_path.read_text() + + module = importlib.import_module(f"native.lib.dashboards.{dashboard_id}") + component_fn = getattr(module, dashboard_id) + + return { + "id": dashboard_id, + "source": source, + "component_fn": component_fn, + "dependencies": _extract_dependencies(source), + } diff --git a/native/templates/_dashboard.py b/native/templates/_dashboard.py new file mode 100644 index 0000000..f0093db --- /dev/null +++ b/native/templates/_dashboard.py @@ -0,0 +1,84 @@ +import reflex as rx +from reflex_components_core.el import div + +from components.core.hugeicon import hi +from components.ui.button import button +from native.templates._copy_btn import create_copy_button, generate_component_id + + +def dashboard_demo(component: rx.Component, source: str): + demo_id = generate_component_id() + copy_button, _ = create_copy_button(content=source) + + return div( + div( + div( + div( + button( + hi("SearchIcon", class_name="size-3.5"), + variant="outline", + class_name=( + "size-7 preview-btn bg-background " + "group-data-[view=code]:bg-transparent " + "group-data-[view=code]:border-transparent" + ), + on_click=rx.call_script( + f""" + document.getElementById("demo-{demo_id}").dataset.view = "preview"; + """ + ), + ), + button( + hi("CodeIcon", class_name="size-3.5"), + variant="outline", + size="sm", + class_name=( + "size-7 code-btn border-transparent bg-transparent " + "group-data-[view=code]:border-border " + "group-data-[view=code]:bg-background" + ), + on_click=rx.call_script( + f""" + document.getElementById("demo-{demo_id}").dataset.view = "code"; + """ + ), + ), + class_name="flex items-center gap-1 radius-lg", + ), + class_name="flex items-center gap-2", + ), + div(copy_button, class_name="flex items-center gap-1 px-2"), + class_name="flex items-stretch justify-between p-0.5 pb-0.5", + ), + div( + div( + div( + component, + class_name="w-full", + ), + class_name=( + "w-full flex-1 flex px-2 py-4 sm:p-6 overflow-auto scrollbar-none " + "group-data-[view=code]:hidden" + ), + ), + div( + rx.el.pre( + rx.el.code( + source, + class_name="language-python text-sm", + ), + ), + class_name=( + "w-full max-h-[60rem] overflow-y-auto p-2 hidden group-data-[view=code]:flex" + ), + ), + class_name=( + "relative m-0.5 mt-0 flex min-h-0 flex-1 flex-col " + "overflow-hidden border bg-background " + "dark:shadow-[inset_0_1px_0_0_rgba(255,255,255,0.05)] rounded-md" + ), + ), + id=f"demo-{demo_id}", + data_view="preview", + class_name="group relative flex min-w-0 flex-col border bg-muted/50 min-h-[45vh] rounded-lg", + ) diff --git a/native/templates/_dashboard_example.py b/native/templates/_dashboard_example.py new file mode 100644 index 0000000..7d493a1 --- /dev/null +++ b/native/templates/_dashboard_example.py @@ -0,0 +1,61 @@ +from reflex_components_core.el import div, h3, p, span + +from components.ui.badge import badge +from native.registry.dashboard_introspect import load_dashboard_example +from native.templates._dashboard import dashboard_demo + + +def _dependency_group(label: str, names: list[str]): + if not names: + return div() + return div( + # p( + # label, + # class_name="text-xs font-medium text-muted-foreground uppercase tracking-wide", + # ), + div( + *[ + badge( + name, + class_name="rounded-none", + # class_name="text-xs px-2 py-1 rounded-md bg-muted border border-input", + ) + for name in names + ], + class_name="flex flex-wrap gap-1.5 mt-2", + ), + class_name="flex flex-col", + ) + + +def dashboard_example(dashboard_id: str, *, title: str, description: str = ""): + """dashboard_id="dashboard_01" -> reads native/lib/dashboards/dashboard_01.py, + renders it via dashboard_demo, and adds a metadata panel next to it + (title/description/dependency badges), pulled straight from that file's + source + imports.""" + example = load_dashboard_example(dashboard_id) + component = example["component_fn"]() + deps = example["dependencies"] + + return div( + div( + div( + h3(title, class_name="text-base font-semibold"), + p(description, class_name="text-sm text-muted-foreground mt-1"), + ), + div( + h3("Dependencies", class_name="text-base font-semibold"), + div( + _dependency_group("Components", deps["components"]), + _dependency_group("Blocks", deps["blocks"]), + class_name="flex flex-row items-center gap-2", + ), + class_name="hidden lg:block", + ), + class_name="grid grid-cols-1 lg:grid-cols-2", + ), + div( + dashboard_demo(component, example["source"]), + ), + class_name="w-full flex flex-col gap-6", + ) diff --git a/native/templates/navbar.py b/native/templates/navbar.py index 9236fdb..b830d49 100644 --- a/native/templates/navbar.py +++ b/native/templates/navbar.py @@ -14,6 +14,7 @@ {"title": "Home", "url": ""}, {"title": "Docs", "url": "docs"}, {"title": "Components", "url": "components"}, + {"title": "Dashboards", "url": "dashboards"}, {"title": "Blocks", "url": "blocks"}, {"title": "Typeset", "url": "typeset"}, {"title": "Create", "url": "create"}, diff --git a/native/templates/sublayout.py b/native/templates/sublayout.py index b99eb16..cc9ca81 100644 --- a/native/templates/sublayout.py +++ b/native/templates/sublayout.py @@ -46,7 +46,7 @@ def wrapper(*args, **kwargs): class_name="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between", ), page_content, - class_name="mx-auto flex w-full max-w-5xl flex-col gap-8 px-4 sm:px-2", + class_name="mx-auto flex w-full max-w-6xl flex-col gap-8 px-4 sm:px-2", ), class_name="flex flex-col w-full gap-8", ), diff --git a/scripts/generate_preview_cards.py b/scripts/generate_preview_cards.py index 23d3cf2..87b0ce0 100644 --- a/scripts/generate_preview_cards.py +++ b/scripts/generate_preview_cards.py @@ -55,6 +55,11 @@ "title": "Typeset", "description": "A styling system for HTML and rendered markdown, from blog posts to streaming chat. One CSS file you own.", }, + { + "route": "dashboard", + "title": "Dashboard", + "description": "Build responsive dashboards for analytics, monitoring, and data-driven applications using composable UI components.", + }, ]