diff --git a/docs/prompts/nicegui-ui-customization.md b/docs/prompts/nicegui-ui-customization.md new file mode 100644 index 0000000..95005bd --- /dev/null +++ b/docs/prompts/nicegui-ui-customization.md @@ -0,0 +1,937 @@ +--- +name: nicegui-ui-customization +description: Style, customize, and build interactive UIs in a production-ready NiceGUI app. +--- + +# 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 modules that mount pages and handle page-specific events. Thin adapters that call `services/` for business logic. +- **`ui/components/`**: Reusable presentation components with no business side effects. Accept inputs (props), return UI elements, handle layout and styling concerns. +- **`ui/static/`**: CSS, JS, and image assets loaded at app startup (in `bootstrap.py`), shared across all pages. +- **`services/`**: Business logic orchestration, independent of UI rendering. Called by both pages and API endpoints. +- **`api/`**: HTTP endpoints for API consumers (independent of NiceGUI pages). + +**Dependency direction to maintain**: +- `ui/pages/` → `ui/components/` + `services/` +- `services/` → business logic (repositories, clients, helpers) +- **Avoid**: reverse imports (e.g., `services/` importing from `ui/pages/`) + +--- + +## UI Component Patterns + +Components are the building blocks of reusable UI. They live in `ui/components/` and are consumed by pages in `ui/pages/`. + +### Reusable Component Architecture + +A reusable component is a Python module in `ui/components/` that encapsulates a cohesive piece of UI with no business side effects. + +**Structure**: +```python +# ui/components/card_section.py +from nicegui import ui + +def card_section(title: str, content: str) -> ui.card: + """Reusable card wrapper with consistent styling.""" + 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 +``` + +**When to extract to a component**: +- Visual pattern repeats across multiple pages +- Component has clear input (props) and output (return) +- No business logic; presentation only +- Reusable styles/layout can be applied to similar contexts + +**When to keep in a page**: +- Layout is unique to that page +- Component is tightly coupled to page state/events +- No other page will reuse it + +**Example — Before (duplication)**: +```python +# Pages have repeated card patterns +with ui.card().classes("w-full max-w-md"): + ui.label("Title 1").classes("text-lg font-bold") + # ... repeated layout +``` + +**Example — After (extracted)**: +```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 + +# Pages now use: card_section("Title", "Content") +``` + +### Layout Composition Patterns + +Use containers (`ui.row()`, `ui.column()`) to organize structure; apply Tailwind utility classes for responsive behavior. + +**Example — Responsive grid**: +```python +from nicegui import ui + +with ui.column().classes("w-full"): + with ui.row().classes("w-full gap-4 flex-wrap"): + # On mobile: stack vertically (default block) + # On desktop: row with gap-4 + with ui.card().classes("flex-1 min-w-64"): + ui.label("Card 1") + with ui.card().classes("flex-1 min-w-64"): + ui.label("Card 2") +``` + +**Key principles**: +- Prefer `.classes()` for structure and layout (grid, flex, spacing, responsive) +- Reserve `.style()` for one-off dynamic values computed at runtime +- Use semantic Tailwind classes for clarity (`gap-4`, `flex-1`, `min-w-64`) +- Avoid nested inline styles; centralize reusable patterns in components + +### State Management Between Components + +For simple interactions within a page, use NiceGUI's `ui.state` or signal patterns. + +**Example — Reactive update via signal**: +```python +from nicegui import ui + +# Page-level state +selected_id = ui.state(value=None) + +def item_list(): + """Component that updates page state on selection.""" + with ui.column(): + for item in items: + def on_click(item_id=item.id): + selected_id.value = item_id + ui.button(item.name).on_click(on_click) + +def detail_view(): + """Component that observes state.""" + with ui.card(): + def update_detail(): + if selected_id.value: + detail = load_detail(selected_id.value) + ui.label(f"Details: {detail}") + + selected_id.on_value_changed(update_detail) + update_detail() # Initial render +``` + +**For complex state across multiple pages**: Use a `services/` layer to manage business state, and pass data via page callbacks. Avoid sharing NiceGUI state across pages. + +--- + +## Styling & CSS/JS Injection + +### Tailwind Classes via `.classes()` + +NiceGUI includes TailwindCSS by default. Use `.classes()` for all structural and responsive styling. + +**Example — Mobile-first responsive layout**: +```python +with ui.card().classes("w-full max-w-2xl mx-auto p-4"): + # w-full: full width on mobile + # max-w-2xl: cap at 2xl breakpoint + # mx-auto: center horizontally + # p-4: padding on all sides + + with ui.row().classes("w-full gap-4 flex-wrap sm:flex-nowrap"): + # flex-wrap: wrap on mobile (< 640px) + # sm:flex-nowrap: don't wrap on small screens (>= 640px) + + ui.input("Name").classes("flex-1 min-w-60") + ui.input("Email").classes("flex-1 min-w-60") + + ui.button("Submit").classes("w-full py-2") +``` + +### Custom CSS Injection + +Inject custom CSS at app startup for styling that TailwindCSS doesn't cover (colors, transitions, custom properties). + +**Setup in `bootstrap.py`**: +```python +# src/app/bootstrap.py +from nicegui import ui + +def setup_custom_styles(app): + """Register custom CSS for the entire app.""" + ui.add_css(""" + :root { + --primary-color: #3b82f6; + --border-radius: 8px; + } + + .app-shell { + font-family: system-ui, sans-serif; + background: linear-gradient(135deg, #f5f5f5 0%, #e0e0e0 100%); + } + + .page-section { + border-radius: var(--border-radius); + border: 1px solid #ddd; + transition: all 0.2s ease; + } + + .page-section:hover { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + } + """) +``` + +**In page modules, use semantic class names**: +```python +with ui.column().classes("w-full app-shell"): + with ui.card().classes("page-section"): + ui.label("Content") +``` + +### Static Asset Pipeline + +#### Setup `/app/static/` directory + +```text +src/app/ +├─ static/ +│ ├─ css/ +│ │ ├─ base.css # Resets, common patterns +│ │ ├─ theme.css # Design tokens, colors +│ │ └─ components.css # Component-specific styles +│ ├─ js/ +│ │ └─ custom.js # Rare edge cases requiring JS +│ └─ images/ +└─ ... +``` + +#### Include CSS at app startup + +In `bootstrap.py`, load CSS files once: +```python +def setup_static_assets(app): + """Load static CSS files for all pages.""" + # Load from static folder + ui.add_css(open('src/app/static/css/base.css').read()) + ui.add_css(open('src/app/static/css/theme.css').read()) + ui.add_css(open('src/app/static/css/components.css').read()) +``` + +**Example — `static/css/theme.css`**: +```css +:root { + --color-primary: #3b82f6; + --color-success: #10b981; + --color-error: #ef4444; + --spacing-xs: 0.25rem; + --spacing-sm: 0.5rem; + --spacing-md: 1rem; + --spacing-lg: 1.5rem; + --spacing-xl: 2rem; +} + +.badge-success { + background: var(--color-success); + color: white; + border-radius: 4px; + padding: var(--spacing-xs) var(--spacing-sm); +} + +.badge-error { + background: var(--color-error); + color: white; + border-radius: 4px; + padding: var(--spacing-xs) var(--spacing-sm); +} +``` + +#### Serving static files via FastAPI + +In `bootstrap.py`, mount the static directory: +```python +from fastapi.staticfiles import StaticFiles + +def create_app(): + app = FastAPI() + + # Register API routes, NiceGUI pages, etc. + # ... + + # Mount static files (CSS, JS, images) + app.mount("/static", StaticFiles(directory="src/app/static"), name="static") + + return app +``` + +**Production note**: Behind a reverse proxy (nginx), ensure the proxy correctly rewrites paths. For example, if your app is at `/myapp/`, static files should be served from `/myapp/static/`. + +### CSS Custom Properties for Theming + +Define design tokens as CSS variables for centralized, dynamic theming. + +**Example — `static/css/theme.css`**: +```css +:root { + /* Colors */ + --color-primary: #3b82f6; + --color-background: #f9fafb; + --color-border: #e5e7eb; + + /* Spacing */ + --spacing-unit: 1rem; + + /* Typography */ + --font-family-sans: system-ui, -apple-system, sans-serif; + --font-size-base: 1rem; +} + +body { + font-family: var(--font-family-sans); + background: var(--color-background); +} + +.card { + border: 1px solid var(--color-border); + border-radius: 0.5rem; +} +``` + +**Use in Python via semantic classes**: +```python +ui.card().classes("card") # Automatically picks up CSS variables +ui.label("Text").classes("text-lg") # Uses --font-size-base +``` + +### JS Injection (Rare) + +Use `ui.run_javascript()` only for interactions that NiceGUI APIs don't support. Always prefer NiceGUI methods first. + +**Example — Only if NiceGUI doesn't provide this behavior**: +```python +# Avoid this pattern if NiceGUI has a built-in API +ui.run_javascript(""" + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') { + console.log('User pressed Escape'); + } + }); +""") +``` + +**Better**: Use NiceGUI's event handlers when available. + +--- + +## Event-Driven Patterns + +### File Upload Handling + +Handle uploads with validation; emit success/error notifications. + +**Page module**: +```python +from nicegui import ui +from services.file_service import validate_and_store_file + +@ui.page("/upload") +def upload_page(): + async def handle_upload(e: ui.events.UploadEventArguments): + try: + # Validate file + if e.size > 10 * 1024 * 1024: # 10 MB limit + ui.notify("File too large (max 10 MB)", type="negative") + return + + # Store file + file_path = await validate_and_store_file(e.content.read(), e.name) + + # Notify success + ui.notify(f"File uploaded: {e.name}", type="positive") + + except ValueError as err: + ui.notify(f"Validation error: {err}", type="negative") + except Exception as err: + ui.notify("Upload failed", type="negative") + + ui.upload(on_upload=handle_upload, auto_upload=True) +``` + +**Service module**: +```python +# services/file_service.py +import asyncio +from pathlib import Path + +async def validate_and_store_file(content: bytes, filename: str) -> str: + """Validate and persist file. Raise ValueError on invalid input.""" + if not filename: + raise ValueError("Filename is required") + + # Validate MIME type, size, etc. + allowed_types = {'.txt', '.pdf', '.csv'} + ext = Path(filename).suffix.lower() + if ext not in allowed_types: + raise ValueError(f"File type {ext} not allowed") + + # Store file + output_dir = Path("./uploads") + output_dir.mkdir(exist_ok=True) + output_path = output_dir / filename + + await asyncio.to_thread(output_path.write_bytes, content) + return str(output_path) +``` + +### Form Submission Callbacks + +Collect user input via form callbacks; validate and delegate to service layer. + +**Example — Before (tightly coupled)**: +```python +# Avoid: business logic in page +def on_submit(): + name = name_input.value + # Direct validation in page + if not name: + ui.notify("Name required") + return + # Direct DB call in page (bad pattern) + db_session.add(User(name=name)) + db_session.commit() + ui.notify("User created") +``` + +**Example — After (service-oriented)**: +```python +# Page: thin adapter +async def on_submit(): + try: + result = await user_service.create_user(name_input.value) + ui.notify(f"User created: {result.id}", type="positive") + except ValueError as err: + ui.notify(f"Validation error: {err}", type="negative") + except Exception as err: + ui.notify("Failed to create user", type="negative") + +ui.button("Submit").on_click(on_submit) + +# Service: orchestrates logic +async def create_user(name: str) -> UserDTO: + if not name or len(name) < 2: + raise ValueError("Name must be at least 2 characters") + # Delegate to repository/DB layer + user = await repository.create_user(name) + return UserDTO.from_orm(user) +``` + +### WebSocket & Server-Sent Events (SSE) + +For real-time updates without polling, use WebSocket or SSE. + +**SSE pattern (simpler, one-way updates)**: +```python +# api/events.py +from fastapi import APIRouter +from fastapi.responses import StreamingResponse +import asyncio + +router = APIRouter() + +@router.get("/events/status") +async def status_stream(): + """Stream status updates to client.""" + async def event_generator(): + while True: + status = await get_current_status() # Service call + yield f"data: {status}\n\n" + await asyncio.sleep(1) # Emit every 1 second + + return StreamingResponse(event_generator(), media_type="text/event-stream") + +# In NiceGUI page: +@ui.page("/dashboard") +def dashboard(): + with ui.card(): + status_label = ui.label("Loading...") + + async def listen_for_updates(): + async with aiohttp.ClientSession() as session: + async with session.get("/api/events/status") as resp: + async for line in resp.content: + if line.startswith(b"data:"): + status = line.decode().replace("data:", "").strip() + status_label.set_text(f"Status: {status}") + + ui.timer(0.1, lambda: asyncio.create_task(listen_for_updates())) +``` + +### Background Tasks with UI Signaling + +Long-running operations should run in background tasks; signal UI via updates or notifications. + +**Example — Before (blocking)**: +```python +# Avoid: blocks UI while processing +def on_process(): + for i in range(1000000): + expensive_calculation(i) # Blocks UI + ui.notify("Done") +``` + +**Example — After (background task)**: +```python +from fastapi import BackgroundTasks +import asyncio + +# API endpoint +@app.post("/process") +async def process(background_tasks: BackgroundTasks): + background_tasks.add_task(long_running_job) + return {"status": "processing"} + +# Long-running task +async def long_running_job(): + for i in range(1000000): + await asyncio.sleep(0) # Yield control + expensive_calculation(i) + # Signal UI via WebSocket or polling endpoint + await cache.set("process_status", "complete") + +# Page: poll endpoint for completion +@ui.page("/process") +def process_page(): + async def trigger(): + response = await client.post("/api/process") + ui.notify("Processing started") + + async def check_status(): + status = await client.get("/api/process/status") + if status == "complete": + ui.notify("Processing complete", type="positive") + + ui.button("Start").on_click(trigger) + ui.timer(1, check_status) # Poll every 1 second (fallback only) +``` + +### Reactive Refresh Patterns + +Use `@ui.refreshable` for components that need periodic updates, but prefer event-driven triggers. + +**Example — Refreshable list with manual refresh button**: +```python +@ui.page("/items") +def items_page(): + @ui.refreshable + async def item_list(): + items = await item_service.list_items() + with ui.column(): + for item in items: + ui.label(f"{item.name}: {item.status}") + + async def on_refresh(): + item_list.refresh() + + ui.button("Refresh").on_click(on_refresh) + item_list() +``` + +**Avoid**: `ui.timer()` polling everything. Use timers only for monitoring that cannot be event-driven. + +--- + +## Advanced Interactions + +### Dialogs & Modals + +**Simple dialog via `ui.dialog()`**: +```python +from nicegui import ui + +@ui.page("/form") +def form_page(): + dialog = ui.dialog() + + async def show_confirm(): + with dialog: + ui.label("Are you sure?") + with ui.row(): + ui.button("Yes", on_click=lambda: ( + confirm_action(), + dialog.close() + )) + ui.button("No", on_click=dialog.close) + + dialog.open() + + ui.button("Delete").on_click(show_confirm) + +def confirm_action(): + ui.notify("Confirmed!", type="positive") +``` + +**Complex modal workflow**: Wrap dialog logic in a component for reusability. + +```python +# ui/components/confirm_dialog.py +from nicegui import ui +from typing import Callable + +async def show_confirm_dialog( + title: str, + message: str, + on_confirm: Callable +): + """Reusable confirm dialog.""" + dialog = ui.dialog() + + with dialog: + ui.label(title).classes("text-lg font-bold") + ui.label(message) + with ui.row(): + async def confirm(): + await on_confirm() + dialog.close() + ui.button("Confirm").on_click(confirm) + ui.button("Cancel").on_click(dialog.close) + + dialog.open() +``` + +### Progress Indicators + +**Simple progress bar**: +```python +@ui.page("/upload") +def upload_page(): + progress_bar = ui.linear_progress(value=0).classes("w-full") + + async def handle_upload(e: ui.events.UploadEventArguments): + total = e.size + uploaded = 0 + + # Simulate chunked upload + for chunk in read_in_chunks(e.content, chunk_size=1024): + uploaded += len(chunk) + progress_bar.set_value(uploaded / total) + await asyncio.sleep(0.01) # Simulate processing + + ui.notify("Upload complete", type="positive") + + ui.upload(on_upload=handle_upload) +``` + +### Drag & Drop (via Quasar Fallback) + +NiceGUI doesn't have built-in drag-and-drop; use Quasar internals for this advanced pattern. + +**Practical approach** (minimal JS): +```python +from nicegui import ui + +@ui.page("/dnd") +def dnd_page(): + # Use Quasar's q-sortable for drag-and-drop lists + items = ["Item 1", "Item 2", "Item 3"] + + with ui.column(): + ui.html(f""" +
+
Drag items to reorder
+
+ {''.join(f'
{item}
' for item in items)} +
+
+ """) +``` + +**Trade-off note**: Direct HTML/Quasar usage bypasses NiceGUI's reactivity. Prefer NiceGUI component APIs when possible; use raw HTML only for interactions NiceGUI doesn't support. + +--- + +## Troubleshooting Matrix + +### Upload Validation & Size/Type Errors + +**Problem**: Upload fails silently or accepts invalid files. + +**Checklist**: +- [ ] Validate file size before storage: `if e.size > MAX_BYTES` +- [ ] Validate MIME type or file extension: `if not filename.endswith('.pdf')` +- [ ] Return early on validation failure; don't attempt storage +- [ ] Catch exceptions in `handle_upload` and emit `ui.notify()` with type "negative" +- [ ] Log errors to see why upload failed: `logger.error(f"Upload error: {err}")` + +**Example fix**: +```python +async def handle_upload(e): + try: + if e.size > 10 * 1024 * 1024: + raise ValueError("File too large") + if not e.name.endswith('.pdf'): + raise ValueError("Only PDF files allowed") + # ... proceed + except ValueError as err: + ui.notify(str(err), type="negative") + logger.warning(f"Upload rejected: {err}") +``` + +### Refresh/Update Race Conditions + +**Problem**: Multiple refresh/update requests cause UI flicker, duplicate data, or inconsistent state. + +**Checklist**: +- [ ] Use async/await to serialize updates: `await service.get_data()` before updating UI +- [ ] Avoid multiple timers updating the same component +- [ ] If using `@ui.refreshable`, call `.refresh()` explicitly; don't auto-refresh on every event +- [ ] Add a loading state or disable button during async work: `button.enabled = False` / `button.enabled = True` after completion +- [ ] Test with slow network (dev tools throttle) to catch race conditions + +**Example fix**: +```python +async def on_click(): + button.enabled = False + try: + data = await service.fetch_data() # Await before UI update + list_component.refresh() + finally: + button.enabled = True +``` + +### Asset Loading & Caching Issues + +**Problem**: CSS or JS not applying; styles stale after deploy. + +**Checklist**: +- [ ] Verify CSS is injected at app startup (in `bootstrap.py`), not per-page +- [ ] Check browser DevTools Network tab: are CSS files loading with correct path? +- [ ] Clear browser cache (Ctrl+Shift+Del or DevTools Cache Storage) +- [ ] For production, use cache-busting query params: `link href="/static/css/base.css?v=20240101"` +- [ ] Verify `/static/` mount path matches file system structure +- [ ] Check reverse proxy (nginx) is not caching static files incorrectly + +**Example cache-busting**: +```python +import hashlib +from pathlib import Path + +def get_css_with_hash(css_path: str) -> str: + """Return CSS URL with file hash for cache-busting.""" + full_path = Path(css_path) + if full_path.exists(): + content_hash = hashlib.md5(full_path.read_bytes()).hexdigest()[:8] + return f"{css_path}?v={content_hash}" + return css_path + +# In bootstrap.py: +css_url = get_css_with_hash("/static/css/base.css") +ui.add_css(f'') +``` + +### Navigation & State Desync + +**Problem**: After navigation, UI shows stale data or component state is lost. + +**Checklist**: +- [ ] Don't store UI state in globals; keep it in `services/` or request-scoped +- [ ] Use `ui.navigate.to()` for client-side navigation; verify URL matches a registered `@ui.page()` +- [ ] After navigate, reload data in the new page's `@ui.page()` decorator +- [ ] If using `@ui.refreshable`, ensure component re-fetches data when needed +- [ ] Test navigation flow end-to-end; check browser history + +**Example pattern**: +```python +# Page A: navigate to Page B +def go_to_detail(item_id: int): + ui.navigate.to(f"/detail/{item_id}") + +# Page B: fetch fresh data on page load +@ui.page("/detail/{item_id}") +def detail_page(item_id: int): + @ui.refreshable + async def load_detail(): + item = await service.get_item(item_id) # Fetch fresh + ui.label(item.name) + + load_detail() +``` + +### Deployment & Reverse Proxy Static Path Issues + +**Problem**: Static files return 404 in production behind nginx/reverse proxy. + +**Checklist**: +- [ ] Verify app is mounted at correct path: `/myapp/` vs. `/` vs. subpath +- [ ] Update `docs_mount_path`, static paths in proxy config +- [ ] Test static file URLs in browser: `/static/css/base.css` should return CSS +- [ ] Nginx should rewrite requests: `location /static { proxy_pass http://app/static; }` +- [ ] Check nginx logs for rewrite errors: `tail -f /var/log/nginx/error.log` +- [ ] If using path prefix, ensure all CSS/JS links are relative or include prefix + +**Example nginx config** (app at `/myapp/`): +```nginx +location /myapp { + proxy_pass http://localhost:8000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +} + +location /myapp/static { + alias /path/to/app/src/app/static; + expires 1d; # Cache static files +} +``` + +--- + +## Implementation Checklist + +### Phase 1: Component Architecture +- [ ] Review existing page modules in `ui/pages/` +- [ ] Identify repeated UI patterns (cards, forms, lists) +- [ ] Extract 2–3 reusable components to `ui/components/` +- [ ] Define component API (inputs, outputs, no side effects) +- [ ] Test components in isolation on a debug page + +### Phase 2: Styling & Assets +- [ ] Create `/app/static/css/` directory structure +- [ ] Write `base.css` with resets and common patterns +- [ ] Write `theme.css` with CSS custom properties (colors, spacing) +- [ ] Inject CSS in `bootstrap.py` at app startup +- [ ] Test Tailwind classes on all pages; verify responsive breakpoints (mobile, tablet, desktop) + +### Phase 3: Event-Driven Interactions +- [ ] Implement file upload with validation in one page module +- [ ] Add form submission callback with error handling +- [ ] Test upload/form via manual UI checklist (success, validation errors, edge cases) +- [ ] Add `uv run pytest` tests for service-layer validation + +### Phase 4: Advanced Features (Optional) +- [ ] Add dialogs/modals if needed +- [ ] Implement progress indicator for long operations +- [ ] Test WebSocket/SSE for real-time updates (if applicable) + +### Phase 5: Troubleshooting & Verification +- [ ] Run Ruff checks on all page/component modules: `uv run ruff check src/app/ui/` +- [ ] Test static file loading in browser DevTools (verify network requests) +- [ ] Test on multiple screen sizes (mobile, tablet, desktop) +- [ ] Verify accessibility: keyboard navigation, color contrast, alt text +- [ ] Deployment check: static files accessible behind reverse proxy + +--- + +## Guardrails + +- **Do not** put all component logic in `ui/pages/`. Extract reusable patterns to `ui/components/`. +- **Do not** style everything with inline `.style()`. Use `.classes()` and centralized CSS files. +- **Do not** poll for updates when events are available. Use WebSocket/SSE or manual refresh buttons. +- **Do not** call database or service logic directly in page modules. Delegate to `services/`. +- **Do not** inject CSS per-page. Load all CSS at app startup for consistency. +- **Do not** use `ui.run_javascript()` unless NiceGUI APIs don't support the interaction. + +--- + +## Minimal Verification Example + +**Prerequisites**: A FastAPI + NiceGUI project with the structure described above (`src/app/ui/pages/`, `src/app/ui/components/`, `services/`). + +**Test scenario**: +1. Add a reusable card component to `ui/components/` +2. Use it in two different page modules +3. Add custom CSS in `bootstrap.py` with a semantic class name +4. Implement a file upload with validation in one page +5. Manually verify: responsive layout on mobile, upload validation works, styles apply + +**Commands**: +```bash +# Lint components and pages +uv run ruff check src/app/ui/ + +# Test service-layer validation +uv run pytest tests/ + +# Run app and test manually +uv run nicegui src/app/main.py +``` + +--- + +## Source Documentation (recommended references) + +- NiceGUI components overview: + - https://www.nicegui.io/documentation/section_elements +- NiceGUI pages & routing: + - https://www.nicegui.io/documentation/section_pages_routing +- NiceGUI security (input handling, HTML injection): + - https://www.nicegui.io/documentation/section_security +- TailwindCSS utility classes: + - https://tailwindcss.com/docs/utility-first +- Responsive design with Tailwind: + - https://tailwindcss.com/docs/responsive-design +- CSS custom properties (variables): + - https://developer.mozilla.org/en-US/docs/Web/CSS/--* +- FastAPI background tasks: + - https://fastapi.tiangolo.com/tutorial/background-tasks/ +- FastAPI server-sent events (SSE): + - https://fastapi.tiangolo.com/advanced/server-sent-events/ +- WebSocket with FastAPI: + - https://fastapi.tiangolo.com/advanced/websockets/