Component contracts
Start with your first page, then use the dedicated component guides for copyable examples, settings, integration contracts, and troubleshooting.
Shared providers are registered by createAdmin. Page providers are registered by their lazy module before the page mounts. Public JavaScript methods and lifecycle hooks are documented with JSDoc, and ESLint checks documented functions. This is modern JavaScript, not a TypeScript application.
| Provider | Inputs | Responsibility |
|---|---|---|
adminShell | createAdmin configuration | Persistent layout, navigation, route title/focus, global search |
uiPage(index) | Route metadata, generated importers, consumer context | Lazy loading, permission checks, error recovery, focus, and teardown |
uiDialog(id) | Unique string ID | Open/close state, optional context, targeted events |
uiDropdown | None | Disclosure state, arrow navigation, Escape, route cleanup |
uiAccordion(ids, options) | Enabled IDs and optional multiple-panel policy | Disclosure state and keyboard focus |
uiDatePicker(config) | Date model and Flatpickr options. | Date, range, multiple, and time selection with localization and teardown. |
uiPdf(config) | Document URL and optional request headers. | PDF page navigation, zoom, text, and worker cleanup. |
uiChart(config) | Local ApexCharts options and series, optional cancellable loader | Lazy charts, API response state, updates, theme, and teardown |
uiMap(options) | Geometry, markers, optional authorized tiles | Lazy Leaflet rendering and lifecycle |
uiVideo(options) | Media source, optional poster and caption tracks | Native playback, seek, speed, errors, teardown |
uiTerminal(options) | Consumer gateway connection adapter | xterm input/output, resize, theme, cancellation |
uiVnc(options) | Authorized RFB session factory | noVNC viewport, connection events, remote commands |
uiTabs(ids) | Ordered string array | Roving tab focus, arrows, Home/End |
uiMetric(widget) | Reactive metric object | Localized values, safe percentages, SVG trend geometry |
uiDashboard(widgets, key) | Stable widget definitions and preference key | Saved ordering, visibility, sizes, keyboard and drag ordering |
uiSearch | Route metadata supplied by host | Localized navigation search, keyboard selection |
dataTable(options) | Adapter, columns, optional route, row key, open callback | URL state, paging, sorting, selection, cancellation |
Bootstrap an application
import { createAdmin } from '@agon/atrium';import pages from 'virtual:atrium/pages';import appearance from 'virtual:atrium/appearance';import { routes } from './routes.js';import en from '@agon/atrium/locales/en';import fr from '@agon/atrium/locales/fr';import de from '@agon/atrium/locales/de';import it from '@agon/atrium/locales/it';
const app = createAdmin({ namespace: 'my-project', base: import.meta.env.BASE_URL, brand: { name: 'My project', tagline: 'ADMIN WORKSPACE', }, messages: { en, fr, de, it, }, routes, pages, appearance, context: { resourceService, }, groups: [ { id: 'workspace', label: 'workspaceGroup', }, ], workspaces: [ 'Production', ], session: { user: { name: 'Alex', initials: 'AL', }, capabilities: [ '*', ], },});
app.start();The generated appearance configuration supplies build defaults and saved-preference behavior. See the dedicated appearance configuration guide for setting defaults and disabling the customizer.
The shared header links to /profile, /settings, /notifications, and /login. The starter supplies these routes. Override the header partial when your product uses different destinations. footer is an optional message key, and onLogout is a consumer callback. The context selector is a presentation example. Attach your own workspace adapter before presenting it as a real tenant switch.
Lazy pages
Declare a route with an application-relative template and optional controller module:
const routes = [ { path: '/resources', label: 'resources', template: 'pages/resources/list.html', module: 'pages/resources/list.js', },];The template contains one root, for example <section x-data="resourceList">...</section>. The corresponding JavaScript module exports register(context):
import { dataTable } from '@agon/atrium';
/** * Register this page's provider after its route module loads. * * @param {object} context - Explicit application dependencies. * @param {object} context.Alpine - Shared Alpine runtime. * @param {object} context.resourceService - Consumer-owned backend adapter. * @returns {void} Makes the provider available before the page mounts. */export function register({ Alpine, resourceService,}) { Alpine.data( 'resourceList', /** * Create independent table state for this page instance. * * @returns {object} Resource table provider. */ () => dataTable({ path: '/resources', columns: resourceColumns, /** * Delegate paged loading to the host adapter. * * @param {object} query - Normalized query and cancellation signal. * @returns {Promise<object>} Result page from the backend. */ load: (query) => resourceService.list(query), }), );}resourceColumns and resourceService are application-owned definitions. Register providers without starting timers or requests in register itself. Component init() owns startup and destroy() owns cleanup. A module is cached by the browser, but registration may run again when a new page instance mounts. Static pages can omit module. Shared page controllers may be referenced by several routes and Vite will reuse their dependency chunks.
A failed chunk shows a recoverable page error while the shell remains usable. Retry attempts initialization again. Reload application performs a full document reload when the browser has cached a module failure. Regular successful navigation preserves the document lifecycle.
Shared request progress
The returned application exposes app.progress, also provided as progress in lazy page registration context and $store.progress in Alpine. It tracks overlapping application operations and drives the shell’s global loading line alongside page navigation. Pass it to createHttpClient({ progress }) or wrap custom adapters with progress.track(operation). See progress integration for lifecycle, cancellation, startup markup, and the large-response demonstration.
Localization
Atrium includes complete English (en), French (fr), German (de), and Italian (it) dictionaries. Register the dictionaries in messages as shown above. The showcase settings and shared theme customizer offer all four languages. Language changes update the document language, translated labels, and locale-aware formatting, and persist across reloads. Missing messages fall back to English. Text direction remains a separate preference.
Dialogs and drawers
Include a dialog with a stable ID and a consumer-owned body:
<include src="{{ uiRoot }}/dialog.html" locals='{"id":"edit-record","title":"editResource","content":"partials/editor.html"}'></include>Open it with openDialog('edit-record', optionalContext) or $dispatch('dialog-open', { id: 'edit-record' }). Close it with the close() method inside the dialog or a targeted dialog-close event. payload holds optional context, while form drafts stay in the consumer controller. Atrium’s cancellable x-ui-trap directive uses focus-trap to own focus, native inert state, and scroll locking, then restores them on close. It also rejects delayed activation after immediate closure or route teardown. Avoid opening multiple modal layers at once.
Tables
import { dataTable } from '@agon/atrium';
/** * Create a host-owned resource list. * * @returns {object} Alpine provider. */function resourceList() { return dataTable({ path: '/resources', rowKey: 'id', columns: [ { id: 'name', label: 'name', type: 'title', sortable: true, }, { id: 'status', label: 'status', type: 'status', sortable: true, }, ], /** * Load records using the normalized table query. * * @param {object} query - Filters, paging, sorting, and cancellation signal. * @returns {Promise<{items: Array<object>, total: number}>} Result page. */ load: (query) => resourceService.list(query),
/** * Navigate to the selected resource's detail route. * * @param {object} record - Selected resource. * @param {object} table - Active table controller. * @returns {void} */ open: (record, table) => { table.$router.push(`/resources/${record.id}`); }, });}The adapter returns { items, total }, filtering and sorting the complete collection before paging. Column types are title, status, progress, or plain text (omit type). Use translate: true for enum message keys. The shared title cell expects name and optionally description, email, or initials. For a different cell composition, replace the partial while keeping the controller.
When extending a provider, use extendComponent(dataTable(options), { ...pageMethods }). Do not spread a provider object: object spread evaluates getters immediately, which breaks reactive derived state such as page counts.
Selection is explicitly limited to the current page and is cleared on refresh. URL keys are q, status, page, size, sort, and direction. Default values are omitted. The adapter must whitelist supported server-side sort fields. A supplied rowKey controls both rendering identity and selection. Error and empty states are distinct.
Dashboard and metrics
A widget has { id, label, size, kind } plus host-owned content. Size tokens are s (quarter), m (third), l (half), and xl (full width), with responsive overrides. Only ID, size, visibility, and view preferences are persisted. Use a distinct preference key per user/workspace where needed.
Metrics use { label, value, unit, percent, series, icon, tone, trend, description, decimals }. Missing values render as an em dash rather than zero. Percentages are bounded. series is an array of numeric samples. Invalid samples are omitted. The SVG is a compact trend, not a time-axis analytical chart. Missing samples do not retain a visible time gap.
Drag ordering is an enhancement. The studio’s up/down buttons provide a keyboard and touch alternative. Widget definitions retain stable object identities when dashboard data refreshes.
Templates and CSP expressions
Keep calculations in named JavaScript methods/getters. The CSP evaluator intentionally rejects inline arrow functions, arbitrary globals, and direct DOM property assignment. For example use hasErrors, not Object.keys(errors).length. Use syncSelection($el), not $el.indeterminate = value. Dynamic content is text, not executable HTML.
Use native labels, x-id for repeated control IDs, explicit button types in forms, and x-cloak for initially hidden overlays. Respect prefers-reduced-motion when adding transitions. Route controllers own timer/request cleanup through destroy().
Author
Laurent Declercq l.declercq@agon-innovation.ch
License
Unless otherwise stated all source code is licensed under LGPL 2.1 and has the following copyright:
© 2026, Agon Partners Innovation AG, All rights reserved.The design material and the “Agon Ātrium” trademark is the property of their authors. Reuse of them without prior consent of their respective authors is strictly prohibited.
Version
Version: 20260921