diff --git a/docs/skills/nicegui/SKILL.md b/docs/skills/nicegui/SKILL.md index c7d747a..311edd0 100644 --- a/docs/skills/nicegui/SKILL.md +++ b/docs/skills/nicegui/SKILL.md @@ -3,7 +3,7 @@ name: nicegui description: 'Reference hub for NiceGUI and FastAPI application structure, UI composition, styling, bindable state, interactions, troubleshooting, testing, and source documentation. Use when planning, implementing, reviewing, or debugging NiceGUI applications; load only the references relevant to the task.' x-personal-mcp: id: nicegui - version: 2.0.0 + version: 2.2.0 tags: - nicegui - fastapi @@ -57,8 +57,13 @@ Load [architecture and styling](./references/architecture-and-styling.md) for: - page, component, and service boundaries - component extraction decisions -- Tailwind and Quasar styling order +- Quasar props, Tailwind utilities, and custom CSS boundaries - responsive layout and static asset conventions +- uniformly scaling dialogs on mobile +- preserving Quasar field proportions +- keeping detached `QSelect` menus anchored +- sizing scrollable dialog cards under CSS `zoom` +- validating zoomed controls with Playwright or a browser ### Bindable State diff --git a/docs/skills/nicegui/references/architecture-and-styling.md b/docs/skills/nicegui/references/architecture-and-styling.md index 04c7195..13eaa95 100644 --- a/docs/skills/nicegui/references/architecture-and-styling.md +++ b/docs/skills/nicegui/references/architecture-and-styling.md @@ -1,31 +1,54 @@ -# Architecture and Styling Reference +# NiceGUI Page Layout And Styling -## Project Boundaries +Use this reference to structure NiceGUI pages, choose component boundaries, apply responsive layout, and introduce custom CSS without fighting Quasar's internal geometry. -Use this dependency direction: +## Ownership And Dependency Boundaries + +Keep dependencies flowing in one 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 +- bootstrap code mounts static assets and loads shared CSS once Suggested module split: ```text -src/app/ - ui/pages/ - ui/components/ - ui/static/ +src/my_app/ + ui/ + pages/ + components/ + static/ services/ api/ - bootstrap.py ``` -## Component Extraction Rules +Page modules should compose a route from reusable presentation and service calls. They should not own domain rules, persistence, or long-running synchronous work. -Extract to ui/components when a pattern appears in two or more pages. +## Page Composition -Keep in-page if the layout is specific to a single route. +Build the outer layout before styling individual controls: + +1. Define the page shell and width constraints. +2. Establish responsive rows, columns, gaps, and wrapping. +3. Add semantic sections and repeated components. +4. Configure Quasar component appearance with props. +5. Add custom CSS only for behavior that props and utilities cannot express safely. + +```python +with ui.column().classes("w-full max-w-6xl mx-auto gap-6 px-4"): + page_header(title="Inventory") + + with ui.row().classes("w-full gap-4 flex-wrap lg:flex-nowrap items-start"): + filters_panel().classes("w-full lg:w-72 shrink-0") + item_grid().classes("w-full flex-1 min-w-0") +``` + +Use stable width, minimum-width, and flex constraints so labels, icons, validation messages, and loaded content do not shift the surrounding layout. + +## Component Extraction + +Extract a presentation pattern to `ui/components/` when it appears on two or more pages or when it owns a meaningful interaction boundary. Keep one-off route layout in the page module. ```python def card_section(title: str, content: str) -> ui.card: @@ -35,43 +58,250 @@ def card_section(title: str, content: str) -> ui.card: 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") -``` +Reusable components should accept data and event callbacks rather than import page state or business services implicitly. ## 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) +NiceGUI wraps Quasar components. Choose the styling mechanism according to what it owns: + +1. Use Quasar props for component appearance, density, labels, and popup behavior. +2. Use NiceGUI `.classes()` and Tailwind utilities for width, spacing, alignment, and responsive layout. +3. Use reusable component functions for repeated visual patterns. +4. Use `.style()` for genuinely dynamic inline values. +5. Use minimal shared CSS only when props and utilities are insufficient. + +Common Quasar props include: + +- `outlined` +- `dense` +- `stack-label` +- `popup-content-class` +- `input-class` +- `input-style` + +Avoid overriding internal selectors such as: + +- `.q-field__label` +- `.q-field__native` +- `.q-field__control` +- `.q-field__input` + +Quasar coordinates field height, padding, labels, values, icons, and floating-label transforms. Changing only one internal part tends to cause clipping or overlap. + +## Responsive Layout + +Use Tailwind breakpoint classes for ordinary page adaptation: ```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()) +with ui.row().classes("w-full gap-4 flex-wrap sm:flex-nowrap"): + ui.card().classes("w-full sm:flex-1 sm:min-w-64") + ui.card().classes("w-full sm:flex-1 sm:min-w-64") ``` -## Static Asset Rules +- Start with a usable mobile layout, then add larger breakpoint behavior. +- Allow dense toolbars to wrap or collapse intentionally. +- Use `min-w-0` on flexible content that must shrink inside a row. +- Keep controls and primary actions visible without horizontal scrolling. +- Test the longest realistic labels, values, errors, and menu options. -- Keep custom CSS small and tokenized with variables. -- Avoid per-page CSS injection. -- Verify static mount paths and reverse proxy rewrites. +## Static Assets And Shared CSS -## Links +- Mount static assets from the composition layer. +- Load shared CSS once rather than injecting it from individual pages. +- Keep custom CSS tokenized with variables and scoped to application classes. +- Avoid broad rules against Quasar internals. +- Verify mount paths, reverse-proxy rewrites, and cache behavior. + +```python +from pathlib import Path + +from fastapi.staticfiles import StaticFiles + +STATIC_DIR = Path(__file__).parent / "ui" / "static" + +app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static") +ui.add_css((STATIC_DIR / "css" / "base.css").read_text(encoding="utf-8")) +``` + +## Responsive Dialog Pattern + +Use whole-card scaling when a form dialog must become uniformly larger on mobile while preserving Quasar's internal proportions. Keep detached select menus unscaled and make the card itself scrollable. + +### Use Normal Field Density + +Normal Quasar fields are approximately `56px` high, while dense fields are approximately `40px` high. Remove `dense` when larger controls are needed. + +```python +ui.input("Name").props("outlined") +ui.number("Quantity").props("outlined") +ui.select(...).props( + "outlined popup-content-class=app-item-detail-menu" +) +ui.textarea("Description").props("outlined autogrow") +``` + +Add a scoped class to the dialog card: + +```python +ui.card().classes("app-detail-card app-item-detail-card") +``` + +### Scale The Complete Card + +```css +:root { + --item-dialog-scale: 1; + --item-dialog-max-height: calc(100dvh - 3rem); +} + +.app-item-detail-card { + width: min(50rem, 50vw); + max-height: var(--item-dialog-max-height); + overflow-y: auto; + overscroll-behavior: contain; + zoom: var(--item-dialog-scale); +} + +/* Restore Quasar's baseline if a global rule overrides it. */ +.app-item-detail-card .q-field, +.app-item-detail-menu { + font-size: 14px; +} + +@media (max-width: 599px) { + :root { + --item-dialog-scale: 1.2; + /* 75dvh becomes 90dvh after 1.2x zoom. */ + --item-dialog-max-height: 75dvh; + } + + .app-item-detail-card { + width: 80vw; + } + + .app-item-detail-menu { + font-size: 16.8px; + } +} +``` + +The main mobile tuning knob is: + +```css +--item-dialog-scale: 1.2; +``` + +### Keep Detached Popups Unscaled + +Do not apply `zoom` or `transform: scale()` to a `QSelect` popup menu. Quasar renders menus outside the dialog and positions them from the unscaled anchor geometry. Scaling the menu container afterward separates it from its field. + +Avoid: + +```css +.app-item-detail-card, +.app-item-detail-menu { + zoom: 1.2; +} +``` + +Use: + +```css +.app-item-detail-card { + zoom: 1.2; +} + +.app-item-detail-menu { + font-size: 16.8px; +} +``` + +Use `popup-content-class=app-item-detail-menu` to target the detached menu and enlarge its text without changing its coordinate system. + +### Account For Zoom When Scrolling + +The card's pre-zoom maximum height must account for the scale: + +$$ + ext{pre-zoom max height} += +\frac{\text{desired visible height}}{\text{scale}} +$$ + +For a desired visual height of `90dvh` at $1.2\times$: + +$$ +90 / 1.2 = 75 +$$ + +Therefore: + +```css +--item-dialog-max-height: 75dvh; +``` + +Apply scrolling to the card itself: + +```css +.app-item-detail-card { + max-height: var(--item-dialog-max-height); + overflow-y: auto; + overscroll-behavior: contain; +} +``` + +This keeps the dimmed page stationary while the form scrolls. + +### Match The Quasar Breakpoint + +Quasar's extra-small breakpoint ends at `599.98px`. A mobile-only rule can use: + +```css +@media (max-width: 599px) { + /* Mobile rules. */ +} +``` + +Confirm custom breakpoint values against the target application's Quasar configuration. + +## Validation Checklist + +Test the complete page at representative mobile and desktop viewports. For a dialog, include a mobile viewport such as $390 \times 844$. + +- Page sections do not overlap or introduce unintended horizontal scrolling. +- Responsive rows wrap or resize as designed. +- Dialog remains inside the viewport. +- Dialog has `scrollHeight > clientHeight` when its content is taller than its maximum height. +- Scrolling reaches the final form field. +- Select menus open directly against their fields. +- Menus have no horizontal overflow. +- Values and floating labels are not clipped. +- Select arrows and other icons scale with the card. +- Detached popup menus report `zoom: 1`. + +As a precision check, compare the menu edge with the field edge using browser geometry. One corrected implementation measured within approximately `0.5px` horizontally and `0.14px` vertically; treat those values as an example observation, not a framework guarantee. + +## Playwright Caveat + +Playwright locator clicks can calculate incorrect coordinates for elements inside CSS `zoom`. A failed locator click does not necessarily mean browser interaction is broken. + +For verification, either: + +- click using manually adjusted visual coordinates +- trigger the element through DOM evaluation +- test the interaction manually in a real browser + +Do not alter otherwise correct component styling solely to accommodate this automation limitation. + +## Sources !!! info "Primary sources" - - [NiceGUI elements](https://nicegui.io/documentation/element) + - [NiceGUI element styling and props](https://nicegui.io/documentation/element) - [NiceGUI binding properties](https://nicegui.io/documentation/section_binding_properties) - - [Tailwind utility-first styling](https://tailwindcss.com/docs/utility-first) - - [Quasar components](https://quasar.dev/vue-components) \ No newline at end of file + - [Quasar components](https://quasar.dev/vue-components) + - [Quasar field](https://quasar.dev/vue-components/field/) + - [Quasar select](https://quasar.dev/vue-components/select/) + - [Quasar breakpoints](https://quasar.dev/style/breakpoints/) + - [Tailwind responsive design](https://tailwindcss.com/docs/responsive-design) + - [MDN `zoom`](https://developer.mozilla.org/en-US/docs/Web/CSS/zoom) + - [Playwright input actions](https://playwright.dev/docs/input) \ No newline at end of file diff --git a/zensical.toml b/zensical.toml index 9537f96..e518166 100644 --- a/zensical.toml +++ b/zensical.toml @@ -103,7 +103,7 @@ nav = [ { "NiceGUI" = [ { "Overview" = "skills/nicegui/SKILL.md" }, { "App Architecture" = "skills/nicegui/references/architecture.md" }, - { "Style" = "skills/nicegui/references/architecture-and-styling.md" }, + { "Layout and Style" = "skills/nicegui/references/architecture-and-styling.md" }, { "Binding" = "skills/nicegui/references/binding-dataclasses.md" }, { "Flows" = "skills/nicegui/references/interaction-patterns.md" }, { "Quality" = "skills/nicegui/references/troubleshooting-and-quality-gates.md" },