# Lumi plugin UI guide Plugin pages are Lumi product surfaces. They use the shared EJS layout, tokens, components, request contract, dialog behavior, accessibility rules, and browser verification; plugins must not ship a parallel generic component library. ## Page composition Render through the shared layout and pass a deliberate `pageWidth`: `form` for bounded configuration, `standard` for ordinary member/admin pages, `wide` for list-detail or data-heavy pages, and `workspace` only for a real editor. Use the shared page/section headers, stacks, clusters, fields, buttons, notices, badges, table wrappers, disclosures, and state surfaces. Organize fields around the user's decision rather than storage order. Keep one primary action per region, quieter secondary actions nearby, tertiary work in a named disclosure/menu, and destructive actions spatially separated with the existing contextual safeguard. ## Forms and enhanced responses Keep a functional method/action fallback on every form. Classify it as settings, entity, immediate action, destructive action, filter/search, or upload/import. - Add `data-lumi-settings-form` only to genuinely related persistent settings. - Add `data-lumi-enhanced-form` to an entity/settings form when its handler can return a targeted result without reconstructing the route. - Do not put action, destructive, filter, or unrelated forms into the shared settings save bar. Enhanced handlers return: ```json { "ok": true, "message": "Saved.", "fieldErrors": {}, "data": {}, "patch": {}, "redirect": null, "reloadRequired": false } ``` Return HTTP 400/422 with `ok: false` and field-name messages for validation. The browser keeps failed input and links errors through `aria-describedby`. Use `reloadRequired` only for an explicit restart/runtime replacement, never to communicate routine success. ## Lifecycle and dialogs Any page opting into soft navigation registers a lifecycle: ```js window.LumiPage.register("plugin-example", { mount(root, { signal }) { window.addEventListener("resize", updatePreview, { signal }); const timer = window.setInterval(refreshStatus, 5000); return () => window.clearInterval(timer); } }); ``` Use the supplied abort signal for global listeners and fetches; dispose timers, observers, sockets, and inserted UI in the returned teardown. Do not re-execute inline IIFEs after content replacement. Open modal surfaces through `LumiDialog` or a compatible `.modal-backdrop[aria-hidden]` state. Give the dialog a programmatic title and optional description. Do not implement a separate focus trap, background isolation, scroll lock, or opener restoration. ## Accessibility and responsive behavior Every control has a stable ID and associated label. Link helper/error text with `aria-describedby`; use `aria-invalid` only while invalid. Icon actions include the object in their accessible name. Tables have semantic headings/captions and use the shared wrapper; choose horizontal scrolling for dense comparison data or labelled list rows for small action tables. The complete workflow must reflow at 320 px and remain usable at 200% zoom, keyboard-only, touch/coarse pointer, reduced motion, High Contrast, and custom theme values. Dragging and hover are optional accelerators, never the only path. Implement loading, empty, partial, error, success, and offline/degraded states where data or providers are involved. Empty and no-results are different. Errors name what failed, what is safe, and a real recovery action. ## Styling rules Plugin CSS belongs in the `features` cascade layer and composes semantic tokens. Do not redefine `.button`, `.card`, `.field`, `.table`, `.modal`, or other shared primitives. Hard-coded colors are reserved for meaningful external brand/media values and still need contrast. Do not use remote fonts, shrink operational text below `0.875rem`, calculate the main layout height from the viewport in JavaScript, or create a nested main vertical scroller. ## Verification Add EJS compile coverage and a representative Playwright/axe route state. Exercise empty, populated, error/unconfigured, permission, and degraded states; test keyboard focus, narrow reflow, theme values, no-reload mutation, and repeat mount/unmount behavior. Run `npm run verify:ux`, the plugin's focused verifier, and `npm run test:ui` before handoff.