# 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.
