diff --git a/skills/nicegui-ui-customization/SKILL.md b/skills/nicegui-ui-customization/SKILL.md index 9084ead..855fd6d 100644 --- a/skills/nicegui-ui-customization/SKILL.md +++ b/skills/nicegui-ui-customization/SKILL.md @@ -1,349 +1,134 @@ --- name: nicegui-ui-customization -description: Style, customize, and build interactive UIs in a production-ready NiceGUI app. +description: 'Design and implement production NiceGUI UIs with reusable components, Tailwind-first styling, event-driven interactions, and troubleshooting for uploads, state, and static assets. Use when building or refactoring NiceGUI pages and interaction flows.' +argument-hint: 'What UI outcome should this workflow produce?' --- -# NiceGUI UI Customization & Component Patterns - -Customize, style, and build interactive user interfaces in a production-ready NiceGUI application. This skill covers component architecture, styling systems, event-driven interactions, and practical troubleshooting for production deployments. - -## Goal - -Create a well-architected, responsive, accessible UI that: -- Separates structure (Python layout) from cosmetics (CSS styling) -- Handles user interactions via event-driven patterns, not polling -- Composes reusable components with clear responsibilities -- Integrates custom styling and assets cleanly -- Troubleshoots common failures (race conditions, asset caching, upload errors) - -## Project Structure & Conceptual Boundaries - -This skill assumes a project organized with clear, modular boundaries: - -```text -src/app/ -├─ ui/ -│ ├─ pages/ # Page-level UI modules (route handlers) -│ │ ├─ __init__.py -│ │ ├─ home.py -│ │ ├─ dashboard.py -│ │ └─ about.py -│ ├─ components/ # Reusable presentation components -│ │ ├─ __init__.py -│ │ ├─ nav.py -│ │ ├─ card_section.py -│ │ └─ form_section.py -│ └─ static/ # Static assets (CSS, JS, images) -│ ├─ css/ -│ │ ├─ base.css -│ │ ├─ theme.css -│ │ └─ components.css -│ ├─ js/ -│ └─ images/ -├─ services/ # Business logic layer -│ ├─ __init__.py -│ ├─ file_service.py -│ ├─ user_service.py -│ └─ ... -├─ api/ # HTTP endpoints -│ ├─ __init__.py -│ ├─ health.py -│ └─ routes.py -├─ bootstrap.py # App composition, CSS/asset setup -├─ config.py # Settings and configuration -└─ ... -``` - -**Key boundaries**: -- **`ui/pages/`**: Route-level adapters that call `services/` -- **`ui/components/`**: Reusable presentation components (no business logic) -- **`ui/static/`**: Assets loaded at startup in `bootstrap.py` -- **`services/`**: Business logic (independent of UI) -- **`api/`**: HTTP endpoints - -**Dependency flow**: `pages/` → `components/` + `services/` (no reverse imports) - ---- - -## UI Component Patterns - -Components are the building blocks of reusable UI. They live in `ui/components/` and are consumed by pages in `ui/pages/`. Each component maps to [NiceGUI elements](https://nicegui.io/documentation/element), which expose a consistent API for structure, styling, and event handling. - -### Reusable Components - -Extract to `ui/components/` when patterns repeat across pages. Keep in-page layouts unique to that page. - -```python -# ui/components/card_section.py -def card_section(title: str, content: str) -> ui.card: - with ui.card().classes("w-full max-w-md") as card: - ui.label(title).classes("text-lg font-bold") - ui.label(content).classes("text-gray-600") - return card -``` - -### Layout Patterns - -Use Tailwind for all structural styling. Reserve `.style()` for computed values. +# NiceGUI UI Customization Workflow -```python -with ui.column().classes("w-full"): - with ui.row().classes("w-full gap-4 flex-wrap sm:flex-nowrap"): - ui.card().classes("flex-1 min-w-64") - ui.card().classes("flex-1 min-w-64") -``` - -### State Management - -Use [bindable dataclasses](https://nicegui.io/documentation/section_binding_properties) for automatic two-way binding. - -```python -from nicegui import binding, ui -from dataclasses import field - -@binding.bindable_dataclass -class PageState: - selected_id: int | None = None - items: list = field(default_factory=list) - -state = PageState() -detail_label = ui.label().bind_text_from(state, 'selected_id') - -def on_select(item_id): - state.selected_id = item_id # Auto-updates UI - -state.bind_on_change(lambda: load_detail(), 'selected_id') -``` - ---- - -## Styling - -Use Tailwind for all styling. Use breakpoint prefixes (`sm:`, `md:`, `lg:`) for responsive design. - -### Custom CSS Injection ⚠️ Avoid This Pattern - -**Avoid injecting custom CSS** via `ui.add_css()` at app startup. Custom CSS injection creates maintenance burden, breaks IDE autocomplete, and makes styling harder to reason about. Instead: - -**✅ Prefer these approaches**: -1. **Use [Tailwind utility classes](https://tailwindcss.com/docs/utility-first)** — covers 95% of styling needs with zero CSS overhead. - ```python - with ui.column().classes("w-full bg-gradient-to-br from-gray-100 to-gray-200"): - with ui.card().classes("rounded-lg border border-gray-300 shadow-md hover:shadow-lg transition-shadow"): - ui.label("Content") - ``` - -2. **Use [Quasar props](https://quasar.dev/vue-components)** — high-level component theming built into NiceGUI elements. - ```python - ui.button("Submit").props("color=primary size=lg") - ``` - -3. **Extract reusable components** — encapsulate styling patterns in Python functions rather than CSS classes. - ```python - def styled_card(title: str, content: str) -> ui.card: - with ui.card().classes("rounded-lg border border-gray-300 shadow-md") as card: - ui.label(title).classes("text-lg font-bold") - ui.label(content).classes("text-gray-600") - return card - ``` +Create, style, and ship production NiceGUI UI flows with a repeatable process. The workflow keeps structure in Python, favors Tailwind and Quasar APIs for styling, and uses event-driven interaction patterns over ad-hoc polling. -**If you absolutely must inject CSS** (rare edge cases only): -- Store CSS in `src/app/static/css/` files -- Load via `ui.add_css(open('src/app/static/css/custom.css').read())` in `bootstrap.py` -- Keep injected CSS minimal and well-documented -- Avoid hardcoding colors or spacing; use CSS variables instead +## When To Use -### Static Assets +- Building a new NiceGUI page or dashboard +- Refactoring a page into reusable components +- Adding file upload, form submission, live status, or background-job UX +- Troubleshooting race conditions, stale assets, or inconsistent state updates -Load CSS files at app startup in `bootstrap.py`: -```python -from fastapi.staticfiles import StaticFiles +## Target Outcome -app.mount("/static", StaticFiles(directory="src/app/static"), name="static") -ui.add_css(open('src/app/static/css/base.css').read()) -``` +Deliver a responsive, accessible UI flow that: -Use CSS variables for design tokens: -```css -:root { - --color-primary: #3b82f6; - --spacing-unit: 1rem; -} -``` +- keeps clear boundaries between page adapters, reusable components, and services +- uses Tailwind-first styling with minimal custom CSS +- updates UI through events and bindings +- has validation, user feedback, and failure handling +- passes a production-readiness check at the end -### JS Injection (Rare) +## Progressive Loading References -Use `ui.run_javascript()` only for unsupported interactions. Prefer [NiceGUI APIs](https://nicegui.io/documentation/section_action_events) first. - ---- +Load these references only when needed: -## Event-Driven Patterns +- Architecture and styling rules: [./references/architecture-and-styling.md](./references/architecture-and-styling.md) +- Event and state interaction patterns: [./references/interaction-patterns.md](./references/interaction-patterns.md) +- Troubleshooting and release gates: [./references/troubleshooting-and-quality-gates.md](./references/troubleshooting-and-quality-gates.md) -NiceGUI provides event-driven APIs for [actions and events](https://nicegui.io/documentation/section_action_events). This section covers common patterns: file uploads, form submissions, WebSocket/SSE for real-time updates, and background tasks. +## Procedure -### File Upload +### 1. Define the UI Slice -Validate and notify; delegate storage to service layer. +- Capture the user-visible outcome for this task in one sentence. +- Identify route-level page modules to touch. +- Identify service operations needed by the UI. -```python -async def handle_upload(e: ui.events.UploadEventArguments): - try: - if e.size > 10 * 1024 * 1024: - raise ValueError("File too large") - if not e.name.endswith('.pdf'): - raise ValueError("Only PDF allowed") - await file_service.store(e.content.read(), e.name) - ui.notify(f"Uploaded: {e.name}", type="positive") - except ValueError as err: - ui.notify(str(err), type="negative") +Completion check: -ui.upload(on_upload=handle_upload, auto_upload=True) -``` +- You can name the target page, component candidates, and service calls before coding. -### Form Submission +### 2. Choose Component Extraction Strategy -Bind form inputs to dataclass; validate in service. +Decision point: -```python -@binding.bindable_dataclass -class FormData: - name: str = "" - email: str = "" +- If a layout pattern appears in 2 or more pages, extract it to `ui/components/`. +- If a pattern is page-specific, keep it in the page module. -data = FormData() -ui.input("Name").bind_value(data, 'name') -ui.input("Email").bind_value(data, 'email') +Completion check: -async def on_submit(): - try: - await user_service.create_user(name=data.name, email=data.email) - ui.notify("User created", type="positive") - data.name = data.email = "" - except ValueError as err: - ui.notify(str(err), type="negative") +- Reused UI patterns are encapsulated as callable components. -ui.button("Submit").on_click(on_submit) -``` +### 3. Build Responsive Layout First -### Real-Time Updates (WebSocket / SSE) +- Use Tailwind utility classes for structure and spacing. +- Use responsive breakpoints (`sm:`, `md:`, `lg:`). +- Reserve `.style()` for dynamic values that cannot be expressed with classes. -For streaming updates, use [SSE](https://fastapi.tiangolo.com/advanced/server-sent-events/) (one-way) or [WebSocket](https://fastapi.tiangolo.com/advanced/websockets/) (bidirectional). +Completion check: -```python -# api/events.py -@app.get("/events/status") -async def status_stream(): - async def gen(): - while True: - yield f"data: {await get_status()}\\n\\n" - await asyncio.sleep(1) - return StreamingResponse(gen(), media_type="text/event-stream") +- Layout works at mobile and desktop widths without custom CSS overrides. -# Page -status_label = ui.label() -async def listen(): - async with aiohttp.ClientSession() as s: - async with s.get("/api/events/status") as r: - async for line in r.content: - if line.startswith(b"data:"): - status_label.set_text(line.decode().replace("data:", "").strip()) +### 4. Add Reactive State And Events -ui.timer(0.1, lambda: asyncio.create_task(listen())) -``` +- Use bindable dataclasses for local page state. +- Prefer event handlers (`on_click`, `on_upload`, etc.) over periodic polling. +- Trigger explicit refreshes with `@ui.refreshable` where needed. -### Background Tasks +Decision point by interaction type: -Delegate long-running work to background tasks; signal UI via polling or WebSocket. +- File upload: validate size/type, delegate storage to a service, notify success/failure. +- Form submit: bind inputs to dataclass fields, validate in service layer, clear state on success. +- Real-time status: use SSE or WebSocket for push updates. +- Long jobs: run in background task, update status endpoint or stream. -```python -# API -@app.post("/process") -async def process(background_tasks: BackgroundTasks): - background_tasks.add_task(long_job) - return {"status": "processing"} +Completion check: -# Page -async def on_start(): - await client.post("/api/process") - ui.notify("Started") +- Every user action has explicit positive and negative feedback via `ui.notify()`. -async def check(): - status = await client.get("/api/process/status") - if status == "done": - ui.notify("Complete", type="positive") +### 5. Apply Styling Strategy -ui.button("Start").on_click(on_start) -ui.timer(1, check) -``` +Preferred order: -### Reactive Refresh +1. Tailwind utility classes +2. Quasar props +3. Reusable styled component functions -Use `@ui.refreshable` with explicit refresh triggers; avoid polling everything. +Only if absolutely necessary: -```python -@ui.refreshable -async def item_list(): - items = await service.list() - for item in items: - ui.label(item.name) - -ui.button("Refresh").on_click(lambda: item_list.refresh()) -``` +- Load minimal custom CSS once at startup in `bootstrap.py`. +- Keep custom CSS tokenized (variables) and documented. ---- +Completion check: -## Advanced Interactions +- Styling is mostly class/props-driven and not dependent on scattered ad-hoc CSS. -### Dialogs +### 6. Harden Against Common Failures -```python -dialog = ui.dialog() +- Prevent duplicate submissions by disabling controls during in-flight operations. +- Avoid overlapping timers for the same state target. +- Serialize dependent updates (`await` service call before mutation/render). +- Verify static mount paths and cache behavior for changed assets. -async def show_confirm(): - with dialog: - ui.label("Are you sure?") - ui.button("Yes").on_click(lambda: (confirm_action(), dialog.close())) - ui.button("No").on_click(dialog.close) - dialog.open() +Completion check: -ui.button("Delete").on_click(show_confirm) -``` +- Race conditions and stale asset symptoms are addressed with explicit safeguards. -### Progress +### 7. Final Production Readiness Review -```python -progress = ui.linear_progress(value=0).classes("w-full") +Pass all checks: -async def handle_upload(e): - for chunk in read_chunks(e.content): - progress.set_value(uploaded / total) - ui.notify("Complete", type="positive") -``` +- Structure: pages, components, services follow one-way dependency flow. +- Responsiveness: tested at small and large viewport widths. +- Accessibility: labels, button text, and action visibility are clear. +- Reliability: validation and exception paths produce user-facing notifications. +- Maintainability: repeated UI patterns are extracted; business logic stays in services. -### Drag & Drop +If any check fails, return to the relevant step and iterate. -Use [Quasar q-sortable](https://quasar.dev/vue-components/sortable) for native drag-and-drop support via `.props()`. +## Completion Contract ---- +This workflow is complete when: -## Troubleshooting - -### Upload Validation & Errors -- Validate file size/extension before storage -- Catch exceptions and emit `ui.notify()` with type "negative" -- Log errors for debugging - -### Race Conditions -- Serialize updates: `await service.get_data()` before UI update -- Avoid multiple timers on same component -- Use `button.enabled = False/True` to prevent duplicate submissions - -### Asset Caching -- Inject CSS in `bootstrap.py` at startup, not per-page -- Clear browser cache (Ctrl+Shift+Del) or use cache-busting query params: `?v=hash` -- Verify `/static/` mount path and nginx proxy rewrites - -### Navigation & State -- Don't store state in globals; keep in `services/` or request-scoped -- Use `ui.navigate.to()` to switch pages -- Reload data in destination page's `@ui.page()` decorator +- the page flow meets the target outcome +- architecture boundaries are preserved +- chosen interaction pattern is implemented with explicit success and failure feedback +- troubleshooting checks pass +- production-readiness gate passes diff --git a/skills/nicegui-ui-customization/references/architecture-and-styling.md b/skills/nicegui-ui-customization/references/architecture-and-styling.md new file mode 100644 index 0000000..944e708 --- /dev/null +++ b/skills/nicegui-ui-customization/references/architecture-and-styling.md @@ -0,0 +1,76 @@ +# Architecture and Styling Reference + +## Project Boundaries + +Use this dependency direction: + +- pages import components and services +- components contain presentation logic only +- services contain business logic and do not import UI +- static assets are mounted and loaded once at bootstrap + +Suggested module split: + +```text +src/app/ + ui/pages/ + ui/components/ + ui/static/ + services/ + api/ + bootstrap.py +``` + +## Component Extraction Rules + +Extract to ui/components when a pattern appears in two or more pages. + +Keep in-page if the layout is specific to a single route. + +```python +def card_section(title: str, content: str) -> ui.card: + with ui.card().classes("w-full max-w-md") as card: + ui.label(title).classes("text-lg font-bold") + ui.label(content).classes("text-gray-600") + return card +``` + +## Tailwind-First Layout Pattern + +Use Tailwind utility classes for structure and spacing. +Use breakpoint classes for responsive behavior. +Use .style() only for values that must be computed dynamically. + +```python +with ui.column().classes("w-full"): + with ui.row().classes("w-full gap-4 flex-wrap sm:flex-nowrap"): + ui.card().classes("flex-1 min-w-64") + ui.card().classes("flex-1 min-w-64") +``` + +## Styling Decision Order + +1. Tailwind utility classes +2. Quasar props +3. Reusable styled component functions +4. Minimal custom CSS loaded once at bootstrap (only when needed) + +```python +from fastapi.staticfiles import StaticFiles + +app.mount("/static", StaticFiles(directory="src/app/static"), name="static") +ui.add_css(open("src/app/static/css/base.css").read()) +``` + +## Static Asset Rules + +- Keep custom CSS small and tokenized with variables. +- Avoid per-page CSS injection. +- Verify static mount paths and reverse proxy rewrites. + +## Links + +- NiceGUI elements: https://nicegui.io/documentation/element +- NiceGUI binding: https://nicegui.io/documentation/section_binding_properties +- Tailwind: https://tailwindcss.com/docs/utility-first +- Quasar components: https://quasar.dev/vue-components diff --git a/skills/nicegui-ui-customization/references/interaction-patterns.md b/skills/nicegui-ui-customization/references/interaction-patterns.md new file mode 100644 index 0000000..ab50902 --- /dev/null +++ b/skills/nicegui-ui-customization/references/interaction-patterns.md @@ -0,0 +1,109 @@ +# Interaction Patterns Reference + +## Reactive State + +Use bindable dataclasses for local page state. + +```python +from dataclasses import field +from nicegui import binding, ui + +@binding.bindable_dataclass +class PageState: + selected_id: int | None = None + items: list = field(default_factory=list) + +state = PageState() +ui.label().bind_text_from(state, "selected_id") +``` + +## File Upload Pattern + +- Validate extension and size before storing. +- Delegate storage to a service method. +- Notify success and failure explicitly. + +```python +async def handle_upload(e: ui.events.UploadEventArguments): + try: + if e.size > 10 * 1024 * 1024: + raise ValueError("File too large") + if not e.name.endswith(".pdf"): + raise ValueError("Only PDF allowed") + await file_service.store(e.content.read(), e.name) + ui.notify(f"Uploaded: {e.name}", type="positive") + except ValueError as err: + ui.notify(str(err), type="negative") + +ui.upload(on_upload=handle_upload, auto_upload=True) +``` + +## Form Submission Pattern + +- Bind UI inputs to dataclass fields. +- Perform validation in the service layer. +- Clear form state on success. + +```python +@binding.bindable_dataclass +class FormData: + name: str = "" + email: str = "" + +data = FormData() +ui.input("Name").bind_value(data, "name") +ui.input("Email").bind_value(data, "email") + +async def on_submit(): + try: + await user_service.create_user(name=data.name, email=data.email) + ui.notify("User created", type="positive") + data.name = data.email = "" + except ValueError as err: + ui.notify(str(err), type="negative") + +ui.button("Submit").on_click(on_submit) +``` + +## Real-Time Updates Decision + +Use SSE for one-way status streaming. +Use WebSocket for bidirectional messaging. + +SSE endpoint example: + +```python +@app.get("/events/status") +async def status_stream(): + async def gen(): + while True: + yield f"data: {await get_status()}\\n\\n" + await asyncio.sleep(1) + return StreamingResponse(gen(), media_type="text/event-stream") +``` + +## Background Work Pattern + +- Start long jobs in FastAPI background tasks. +- Expose status via endpoint or streaming channel. +- Guard buttons against duplicate submissions during in-flight tasks. + +## Explicit Refresh Pattern + +Use @ui.refreshable and call refresh intentionally instead of polling unrelated state. + +```python +@ui.refreshable +async def item_list(): + items = await service.list() + for item in items: + ui.label(item.name) + +ui.button("Refresh").on_click(lambda: item_list.refresh()) +``` + +## Links + +- NiceGUI action events: https://nicegui.io/documentation/section_action_events +- FastAPI SSE: https://fastapi.tiangolo.com/advanced/server-sent-events/ +- FastAPI WebSockets: https://fastapi.tiangolo.com/advanced/websockets/ diff --git a/skills/nicegui-ui-customization/references/troubleshooting-and-quality-gates.md b/skills/nicegui-ui-customization/references/troubleshooting-and-quality-gates.md new file mode 100644 index 0000000..e3d4152 --- /dev/null +++ b/skills/nicegui-ui-customization/references/troubleshooting-and-quality-gates.md @@ -0,0 +1,39 @@ +# Troubleshooting and Quality Gates + +## Troubleshooting + +### Upload Errors + +- Validate extension and size before storage. +- Catch expected exceptions and return negative notifications. +- Log unexpected exceptions with request context. + +### UI Race Conditions + +- Disable triggering controls during async work. +- Remove duplicate timers and listeners targeting the same state. +- Ensure service call ordering is deterministic before render updates. + +### Asset Caching + +- Confirm static mount and proxy rewrite correctness. +- Add cache-busting query strings for changed assets. +- Avoid per-page CSS injection. + +### Navigation and State Drift + +- Avoid global mutable UI state. +- Keep state request-scoped or service-managed. +- Rehydrate page data during route load. + +## Production Readiness Gate + +Pass all checks before shipping: + +- Structure: one-way dependencies between pages, components, and services. +- Responsiveness: UI validated at both small and large viewport widths. +- Accessibility: labels and actions are clear and readable. +- Reliability: validation and exception paths surface user feedback. +- Maintainability: repeated UI patterns are extracted; business logic remains in services. + +If any check fails, return to the workflow step that owns that concern.