4.3 KiB
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-formonly to genuinely related persistent settings. - Add
data-lumi-enhanced-formto 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:
{
"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:
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.