@coffer-org/web-ui
v8.0.0
Published
The UI contract for Coffer plugin **web bundles**. A plugin's `dist/web.js` is loaded at runtime by the host's federation loader; this package defines what that bundle declares (`PluginUIBundle`) and the field-renderer registry it plugs into.
Readme
@coffer-org/web-ui
The UI contract for Coffer plugin web bundles. A plugin's dist/web.js is
loaded at runtime by the host's federation loader; this package defines what that
bundle declares (PluginUIBundle) and the field-renderer registry it plugs into.
What a web bundle exports
web.js default-exports a single PluginUIBundle — one bundle, not an array:
export default PluginUIBundle;Build one with definePluginUI(bundle):
import { definePluginUI } from '@coffer-org/web-ui';
export default definePluginUI({
// shelf?: { library, shelf, RecordView? } — only needed for a custom
// RecordView (replaces the generic RecordContent) tied to one shelf
// renderers?: FieldRenderer[] — own field-type renderers (each carries kinds[])
// listViews?: ListViewDef[] — own list-view kinds
// templates?: Record<string, TemplateImporter<any>> — action-result templates
// translations?: Partial<Record<'uk' | 'ru' | 'en', Record<string, unknown>>>
// slots?/routes?/nav?: contributions to the app shell (overlay, SPA routes, sidebar nav)
translations: { en: { 'myplugin.hello': 'Hello from myplugin' } },
});definePluginUI is the identity function — it exists so every plugin's ui.tsx
has one recognizable call, not three. There is no per-plugin choice to make: a
ui.tsx that only needs a RecordView, one that only needs renderers, and one
that only needs slots all call the same factory with the same type.
The host's federation loader applies the bundle once: applyWebBundle
(packages/web/federation.ts) forwards slots/routes/nav to applyAppUI
(this package) and itself registers shelf.RecordView for shelf.library/shelf,
calls registerRenderer/registerListView for renderers/listViews, and merges
translations into the global i18next instance.
Vendor singleton model (importmap)
Plugin web bundles must not ship their own copy of React, i18next, or
@coffer-org/web-ui — they share the host's. The host publishes one vendor bundle
under /vendor/* and wires an import map
so every bare specifier resolves to that single instance:
<script type="importmap">
{
"imports": {
"react": "/vendor/react.js",
"react-dom": "/vendor/react-dom.js",
"i18next": "/vendor/i18next.js",
"@coffer-org/web-ui": "/vendor/web-ui.js"
}
}
</script>This keeps a single React (so hooks/context work across bundles) and a single
i18next (so addResourceBundle merges into the one live instance). In dev, Vite
resolves these specifiers via its own aliases, so the import map is a prod-only
concern.
Supported vendor majors
| Vendor | Range |
| --------------------- | ---------------------------- |
| react / react-dom | 19 (>=19) |
| i18next | 26 (23+ host-compatible) |
A scaffolded plugin declares these as peer dependencies (see
create-coffer-plugin's package.json template), so it builds against the
host's instance instead of bundling its own.
Mismatched vendor versions warn, don't block
The import map always resolves a plugin's react / react-dom / i18next /
@coffer-org/web-ui imports to the host's single instance — there is no runtime
version gate that refuses to load a bundle. A vendor-major mismatch surfaces as a
peer-dependency warning at install/build time, not a hard runtime failure: a
plugin built against an older major still loads against the host's instance and
runs as far as the shared API allows. Keep peers in range to avoid surprises; a
mismatch is a warning, not a block.
