diff --git a/src/personal_mcp/docs/skills/nicegui/SKILL.md b/src/personal_mcp/docs/skills/nicegui/SKILL.md index dbb40db..f6b462f 100644 --- a/src/personal_mcp/docs/skills/nicegui/SKILL.md +++ b/src/personal_mcp/docs/skills/nicegui/SKILL.md @@ -29,6 +29,7 @@ Use this skill to choose the smallest supporting reference for a NiceGUI task. T | Customize `ui.table` or QTable columns, formatting, classes, props, responsive density, toolbar controls, visible columns, empty states, named slots, or frontend methods | [table customization](./references/table-customization.md) | Add [editable tables](./references/tables.md) only when cells also accept server-authoritative edits. | | Make `ui.table` cells editable with stable row keys, dataframe projections, row-scoped dataclasses, validation, touched rows, selection-preserving refresh, or `QPopupEdit` | [editable tables](./references/tables.md) | Follow its links to binding or component mechanics only when changing the underlying projection or event bridge. | | Implement uploads, form submission, SSE versus WebSockets, background jobs, duplicate-submit guards, or `@ui.refreshable` and `@ui.refreshable_method` component regions | [interaction patterns](./references/interaction-patterns.md) | Add [application architecture](./references/architecture.md) for the reusable component contract or [binding dataclasses](./references/binding-dataclasses.md) when state propagation itself is the problem. | +| Build or explain URL-backed tabs, persistent tab panels, `ui.sub_pages` route adapters, browser-history synchronization, or parameterized routes that share one tab | [URL-backed tabs with sub pages](./references/tabbed-subpages.md) | Add [binding dataclasses](./references/binding-dataclasses.md) only when the route-backed state grows beyond the single field shown in the example. | | Investigate upload errors, async UI races, stale assets, navigation/state drift, or perform a compact production-readiness review | [troubleshooting and quality gates](./references/troubleshooting-and-quality-gates.md) | Follow the symptom to one detailed reference above. | | Verify a framework claim against primary NiceGUI, FastAPI, Uvicorn, Tailwind, Quasar, SQLAlchemy, Pydantic, or LangGraph documentation | [source documentation](./references/source-documentation.md) | Use a task page first when implementation guidance, not source lookup, is needed. | @@ -51,6 +52,7 @@ Load an example only when its exact mechanic matches the task: - [select events](./examples/select_events.py): `on_change`, generic Quasar events, `update:model-value`, browser-to-Python payload forwarding, and programmatic value changes. - [table customization](./examples/table_customization.py): raw-value sorting with cosmetic prefix, suffix, and datetime formatting; dynamic classes; QTable props; toolbar and cell slots; filtering; visible columns; and empty states. - [editable table](./examples/editable_table.py): dataframe-to-row state, named QTable cell slots, controlled editors, dialog-based whole-row save/cancel edits, Python validation, touched rows, and canonical row refresh. +- [tabbed sub-pages](./examples/tab_spa.py): a persistent shell with URL-backed tabs, tab panels, browser-history navigation, and retained state for a parameterized report route. See the [reference explanation](./references/tabbed-subpages.md) for the ownership model and behavioral boundaries. ## Defaults That Span References diff --git a/src/personal_mcp/docs/skills/nicegui/references/tabbed-subpages.md b/src/personal_mcp/docs/skills/nicegui/references/tabbed-subpages.md new file mode 100644 index 0000000..ae6f827 --- /dev/null +++ b/src/personal_mcp/docs/skills/nicegui/references/tabbed-subpages.md @@ -0,0 +1,101 @@ +# URL-Backed Tabs with Sub Pages + +The [`tab_spa.py` example](../examples/tab_spa.py) combines [NiceGUI tabs](https://nicegui.io/documentation/tabs) with [sub-page routing](https://nicegui.io/documentation/sub_pages). It keeps one application shell and one set of tab panels mounted while the browser URL identifies the active view. + +The example targets NiceGUI `3.16.0`. In this design, tabs are the visible navigation and content mechanism; `ui.sub_pages` is a URL-matching adapter whose route builders update the tab state instead of rendering route content inside the router. + +## Responsibility Map + +| Surface | Responsibility | +| --- | --- | +| `root()` | Creates one client-local shell, reads the initial URL, and connects the tabs, panels, and router. | +| `ui.tabs` | Holds the selected tab name and emits user selection changes. | +| `ui.tab_panels` | Displays the panel whose name matches the selected tab. | +| `ui.sub_pages` | Matches URL paths, extracts route parameters, and invokes the corresponding route callback without a full page reload. | +| `NavigationState` | Retains the concrete report path represented by the shared `reports` tab. | +| `ui.navigate.to()` | Changes the browser location so the sub-pages router can resolve the destination. | + +The separation matters because a tab name is not always a URL. Static tabs use their route as their name, but every `/reports/{report_id}` URL maps to the single `reports` tab and panel. + +## Route and Panel Mapping + +| Browser path | Tab value | Panel value | Route callback effect | +| --- | --- | --- | --- | +| `/` | `/` | `/` | Selects the overview panel. | +| `/projects` | `/projects` | `/projects` | Selects the projects panel. | +| `/reports/a` | `reports` | `reports` | Stores `/reports/a` and selects the reports panel. | +| `/reports/b` | `reports` | `reports` | Stores `/reports/b` and selects the reports panel. | +| `/settings` | `/settings` | `/settings` | Selects the settings panel. | + +`tab_name_for_route()` is the translation boundary. It preserves static route names, collapses concrete report routes to `reports`, and returns `/` for other paths. + +## Initial Page Construction + +`root()` reads `ui.context.client.sub_pages_router.current_path` before creating the navigation controls. `normalize_route()` removes query strings, fragments, and trailing slashes so `/projects/` and `/projects` select the same tab. + +For a direct request to `/reports/b`, the initial route produces two values: + +- `active_report_path` becomes `/reports/b`. +- the selected tab and panel become `reports`. + +The initial `tabs.set_value(...)` call occurs before `tabs.on_value_change(navigate)` is registered. Initial selection therefore establishes the shell state without treating page construction as a user navigation. Passing the same initial tab value to `ui.tab_panels` aligns the content container with the tabs from the first render. + +All panel builders run during shell construction. Switching tabs changes the selected panel; it does not rerun `overview_page()`, `projects_page()`, `report_page()`, or `settings_page()`. Their element state remains client-local for the lifetime of that shell. + +## Tab-Originated Navigation + +The tab change handler receives the selected tab name. Static tab names are already destinations. The reports tab resolves through `state.active_report_path`, which supplies the last concrete report URL: + +```python +destination = state.active_report_path if tabname == REPORTS_TAB else tabname +ui.navigate.to(destination) +``` + +[`ui.navigate.to()`](https://nicegui.io/documentation/navigate) performs the route transition. The sub-pages router then matches the new location and invokes a callback that selects the corresponding tab. Because the panel container is associated with `tabs`, the visible panel follows that selected value. + +## URL-Originated Navigation + +The route callbacks contain no page markup. They translate router matches back into the visible state: + +```python +def route_reports(report_id: str) -> None: + state.active_report_path = f"/reports/{report_id}" + tabs.set_value(REPORTS_TAB) +``` + +This direction handles direct links and browser back or forward navigation. A URL such as `/reports/b` supplies `report_id="b"`; the callback reconstructs the normalized concrete path, updates report-bound labels through `NavigationState`, and selects the shared reports panel. + +The router element is hidden because it is not the content container in this example: + +```python +ui.sub_pages(routes).classes("hidden") +``` + +Normally, `ui.sub_pages` clears and rebuilds its own children when a route changes. Here its builders only mutate state outside that container, so hiding the empty routing element does not hide the tab-panel content. + +## Parameterized Report State + +`NavigationState.active_report_path` separates tab identity from route identity. The reports tab always has the stable value `reports`, while the state records `/reports/a`, `/reports/b`, or another matched report route. + +This provides two forms of continuity: + +- A direct report URL selects the correct tab and report during initial construction. +- Leaving the reports tab and selecting it again during the same client lifetime returns to the last visited report. + +The state is page-local, not durable storage. Reloading a non-report URL creates a new `NavigationState` and restores `DEFAULT_REPORT_PATH`. Shareable report identity remains durable because report pages encode it in the URL. + +## Behavioral Boundaries + +- `TAB_ROUTES` contains concrete routes whose path and tab identity are the same. Parameterized route families require a stable synthetic tab name such as `reports`. +- `normalize_route()` intentionally ignores query parameters and fragments for tab selection. Route callbacks would need matching parameters if those values affected panel state. +- The hidden router also hides its built-in 404 output. As written, an unmatched path selects the overview panel through `tab_name_for_route()` while the router's not-found content remains invisible. +- Panels are mounted together, so expensive panel construction still occurs during the initial shell build. Lazy or route-specific construction requires a different content ownership model. +- The pattern preserves the shell only for navigation handled by the current `ui.sub_pages` router. A full reload creates a new client and rebuilds all page-local state. + +## Source Index + +!!! info "NiceGUI sources" + - [Sub pages and URL parameters](https://nicegui.io/documentation/sub_pages) + - [Tabs, tab names, and tab panels](https://nicegui.io/documentation/tabs) + - [Navigation and browser history](https://nicegui.io/documentation/navigate) + - [NiceGUI `3.16.0` sub-pages implementation](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/elements/sub_pages.py) \ No newline at end of file