diff --git a/src/personal_mcp/docs/skills/nicegui/examples/tab_spa.py b/src/personal_mcp/docs/skills/nicegui/examples/tab_spa.py new file mode 100755 index 0000000..fa34fea --- /dev/null +++ b/src/personal_mcp/docs/skills/nicegui/examples/tab_spa.py @@ -0,0 +1,178 @@ +#!/usr/bin/env -S uv run --script +# /// script +# dependencies = [ +# "nicegui==3.16.0", +# ] +# /// + +"""Demonstrate URL-backed tabs with persistent parameterized-route state.""" + +from collections.abc import Callable +from dataclasses import dataclass +from urllib.parse import urlsplit + +from nicegui import events +from nicegui import ui +from nicegui.elements.tabs import Tab +from nicegui.elements.tabs import TabPanel +from nicegui.sub_pages_router import SubPagesRouter + +type PageBuilder = Callable[..., None] + +DEFAULT_REPORT_PATH = "/reports/a" +REPORTS_TAB = "reports" + + +def page_heading(title: str, description: str) -> None: + """Render a shared heading for sub-page content.""" + with ui.column().classes("w-full gap-1"): + ui.label(title).classes("text-3xl font-semibold text-stone-900") + ui.label(description).classes("text-base text-stone-600") + + +def overview_page() -> None: + """Render the overview sub-page.""" + with ui.column().classes("w-full max-w-6xl mx-auto gap-6 px-4 py-8"): + page_heading("Overview", "A quick read on the workspace today.") + + metrics = ( + ("Active projects", "8", "folder_open", "primary"), + ("Tasks completed", "24", "task_alt", "positive"), + ("Needs attention", "3", "error_outline", "warning"), + ) + with ui.grid().classes("w-full grid-cols-1 gap-4 md:grid-cols-3"): + for label, value, icon, color in metrics: + with ui.card().classes("w-full p-5 gap-3"): + with ui.row().classes("w-full items-center justify-between"): + ui.label(label).classes("text-sm font-medium text-stone-600") + ui.icon(icon, color=color).classes("text-2xl") + ui.label(value).classes("text-3xl font-semibold text-stone-900") + + +def projects_page() -> None: + """Render the projects sub-page.""" + with ui.column().classes("w-full max-w-6xl mx-auto gap-6 px-4 py-8"): + page_heading("Projects", "Each route builds its own content inside the shared shell.") + + with ui.list().props("bordered separator").classes("w-full bg-white rounded"): + for name, status, color in ( + ("Client portal", "On track", "positive"), + ("Mobile refresh", "In review", "primary"), + ("Data migration", "Blocked", "negative"), + ): + with ui.item(): + with ui.item_section().props("avatar"): + ui.icon("folder", color=color) + with ui.item_section(): + ui.item_label(name) + ui.item_label(status).props("caption") + with ui.item_section().props("side"): + ui.badge(status, color=color) + + +def report_page(report_id: str) -> None: + """Render the report selected by the route parameter.""" + with ui.column().classes("w-full max-w-6xl mx-auto gap-6 px-4 py-8"): + page_heading(f"Report {report_id.upper()}", "The report ID is injected from the URL path.") + + with ui.card().classes("w-full max-w-2xl p-5 gap-4"): + ui.label("Parameterized route").classes("text-xl font-semibold text-stone-900") + ui.label(f"Loaded /reports/{report_id}").classes("text-stone-600") + with ui.row().classes("gap-2"): + ui.button("Report A", on_click=lambda: ui.navigate.to("/reports/a")).props("outline") + ui.button("Report B", on_click=lambda: ui.navigate.to("/reports/b")).props("outline") + + +def settings_page() -> None: + """Render the settings sub-page.""" + with ui.column().classes("w-full max-w-6xl mx-auto gap-6 px-4 py-8"): + page_heading("Settings", "Controls here are recreated when this sub-page is opened.") + + with ui.card().classes("w-full max-w-2xl p-5 gap-4"): + ui.label("Notifications").classes("text-xl font-semibold text-stone-900") + ui.switch("Weekly summary", value=True) + ui.switch("Project status changes", value=True) + ui.switch("Product announcements", value=False) + + +def normalize_route(path: str) -> str: + """Extract and normalize the path portion of a route.""" + return urlsplit(path).path.rstrip("/") or "/" + + +def tab_name_for_route(route: str) -> str: + """Return the tab name associated with a concrete route.""" + if route.startswith("/reports/"): + return REPORTS_TAB + return route if route in ROUTES else "/" + + +@dataclass(slots=True) +class NavigationState: + """Store client-local navigation state for parameterized tabs.""" + + active_report_path: str = DEFAULT_REPORT_PATH + + +type TabHandler = events.ValueChangeEventArguments[str | Tab | TabPanel | None] + + +def create_tabs(router: SubPagesRouter) -> ui.tabs: + """Create route-aware tabs and retain the last selected report.""" + state = NavigationState() + initial_route = normalize_route(router.current_path) + if tab_name_for_route(initial_route) == REPORTS_TAB: + state.active_report_path = initial_route + + def navigate(event: TabHandler) -> None: + """Navigate to the route represented by the selected tab.""" + match event.value: + case str(tabname): + destination = state.active_report_path if tabname == REPORTS_TAB else tabname + ui.navigate.to(destination) + case _: + return + + with ui.column().classes("mx-auto"), ui.tabs() as tabs: + ui.tab("/", label="Overview", icon="space_dashboard") + ui.tab("/projects", label="Projects", icon="folder_open") + ui.tab(REPORTS_TAB, label="Reports", icon="summarize") + ui.tab("/settings", label="Settings", icon="settings") + + tabs.set_value(tab_name_for_route(initial_route)) + tabs.on_value_change(navigate) + + def sync_tab(path: str) -> None: + """Synchronize tab and report state after the route changes.""" + route = normalize_route(path) + tab_name = tab_name_for_route(route) + if tab_name == REPORTS_TAB: + state.active_report_path = route + tabs.set_value(tab_name) + + router.on_path_changed(sync_tab) + + return tabs + + +ROUTES: dict[str, PageBuilder] = { + "/": overview_page, + "/projects": projects_page, + "/reports/{report_id}": report_page, + "/settings": settings_page, +} + + +def root() -> None: + """Build the persistent application shell and sub-page container.""" + router = ui.context.client.sub_pages_router + + with ui.header(elevated=True).classes("py-0 items-center"): + create_tabs(router) + ui.button(icon="settings").classes("text-white").props("round flat").tooltip("Settings") + + ui.sub_pages(ROUTES).classes("w-full") + + +if __name__ in {"__main__", "__mp_main__"}: + ui.run(root, title="Northstar", port=8888, reload=True)