diff --git a/src/personal_mcp/docs/skills/nicegui/examples/tab_spa.py b/src/personal_mcp/docs/skills/nicegui/examples/tab_spa.py index fa34fea..084c9fc 100755 --- a/src/personal_mcp/docs/skills/nicegui/examples/tab_spa.py +++ b/src/personal_mcp/docs/skills/nicegui/examples/tab_spa.py @@ -7,20 +7,22 @@ """Demonstrate URL-backed tabs with persistent parameterized-route state.""" +from __future__ import annotations + from collections.abc import Callable -from dataclasses import dataclass from urllib.parse import urlsplit +from nicegui import binding 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" +TAB_ROUTES = frozenset({"/", "/projects", "/settings"}) def page_heading(title: str, description: str) -> None: @@ -70,14 +72,24 @@ def projects_page() -> None: ui.badge(status, color=color) -def report_page(report_id: str) -> None: - """Render the report selected by the route parameter.""" +def report_page(state: NavigationState) -> None: + """Render report content bound to the active 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.column().classes("w-full gap-1"): + ui.label().bind_text_from( + state, + "active_report_path", + backward=lambda path: f"Report {report_id_from_path(path).upper()}", + ).classes("text-3xl font-semibold text-stone-900") + ui.label("The report ID is injected from the URL path.").classes("text-base text-stone-600") 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") + ui.label().bind_text_from( + state, + "active_report_path", + backward=lambda path: f"Loaded {path}", + ).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") @@ -104,10 +116,10 @@ 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 "/" + return route if route in TAB_ROUTES else "/" -@dataclass(slots=True) +@binding.bindable_dataclass class NavigationState: """Store client-local navigation state for parameterized tabs.""" @@ -117,12 +129,13 @@ class NavigationState: type TabHandler = events.ValueChangeEventArguments[str | Tab | TabPanel | None] -def create_tabs(router: SubPagesRouter) -> ui.tabs: +def report_id_from_path(path: str) -> str: + """Extract the report ID from a normalized report route.""" + return path.rsplit("/", maxsplit=1)[-1] + + +def create_tabs(state: NavigationState, initial_route: str) -> 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.""" @@ -142,36 +155,55 @@ def create_tabs(router: SubPagesRouter) -> ui.tabs: 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 render_tab_panels(tabs: ui.tabs, state: NavigationState, active_tab: str) -> None: + """Render all tabbed page content inside a tab panels container.""" + with ui.tab_panels(tabs, value=active_tab, animated=True).classes("w-full"): + with ui.tab_panel("/"): + overview_page() + with ui.tab_panel("/projects"): + projects_page() + with ui.tab_panel(REPORTS_TAB): + report_page(state) + with ui.tab_panel("/settings"): + settings_page() def root() -> None: """Build the persistent application shell and sub-page container.""" - router = ui.context.client.sub_pages_router + initial_route = normalize_route(ui.context.client.sub_pages_router.current_path) + state = NavigationState() + if tab_name_for_route(initial_route) == REPORTS_TAB: + state.active_report_path = initial_route with ui.header(elevated=True).classes("py-0 items-center"): - create_tabs(router) + tabs = create_tabs(state, initial_route) ui.button(icon="settings").classes("text-white").props("round flat").tooltip("Settings") - ui.sub_pages(ROUTES).classes("w-full") + render_tab_panels(tabs, state, tab_name_for_route(initial_route)) + + def route_overview() -> None: + tabs.set_value("/") + + def route_projects() -> None: + tabs.set_value("/projects") + + def route_reports(report_id: str) -> None: + state.active_report_path = f"/reports/{report_id}" + tabs.set_value(REPORTS_TAB) + + def route_settings() -> None: + tabs.set_value("/settings") + + routes: dict[str, PageBuilder] = { + "/": route_overview, + "/projects": route_projects, + "/reports/{report_id}": route_reports, + "/settings": route_settings, + } + ui.sub_pages(routes).classes("hidden") if __name__ in {"__main__", "__mp_main__"}: