105 lines
4.3 KiB
Markdown
105 lines
4.3 KiB
Markdown
# 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.
|