diff --git a/assets/social/dashboard.webp b/assets/social/dashboard.webp new file mode 100644 index 0000000..24dc8b0 Binary files /dev/null and b/assets/social/dashboard.webp differ 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.", + }, ]