Admin
A generic admin area. It owns no domain features: it provides the shell (an admin layout with a sidebar), a
guarded dashboard page and the registries other modules plug into. Domain admin UIs (forms, events, OAuth
applications, users, ...) stay in their own modules and register here.
@frigear-nu/module-kit, @frigear-nu/ui (the page components
every admin page is built from), @frigear-nu/menu (the sidebar is the admin.nav menus),
@frigear-nu/widget (the dashboard is the admin.dashboard area) and nuxt-auth-utils (the
signed-in user is shown in the account menu).
Optional@frigear-nu/authorization, used for the role guard, and
@frigear-nu/account, which provides the account menu in the sidebar footer.@nuxt/ui is not declared as a module dependency, but the layout and components are built on Nuxt UI, so the host
installs it.Quick setup
export default defineNuxtConfig({
modules: ['@nuxt/ui', '@frigear-nu/authorization', '@frigear-nu/admin'],
admin: {
settings: {
roles: ['admin', 'manager'],
},
},
})
Options
All options live under admin.settings.
| Option | Type | Default | Purpose |
|---|---|---|---|
base | string | '/admin' | Path prefix of the area. A trailing slash is trimmed. |
dashboard | boolean | string | true | The dashboard page at base. false turns it off (other modules' pages still get the layout); a string is a custom path. |
roles | string[] | false | ['admin'] | Roles allowed into the area, applied as a route rule on <base>/**. false sets no rule: guard the area yourself. |
layout | boolean | true | Gives every page under base the admin layout unless it picked its own. |
Behaviour of the generated route rules:
- Guard:
{ roles }on<base>/**, only when@frigear-nu/authorizationis installed. Without it nothing guards the pages on the client and a warning is logged. An explicit rule of yours wins. API routes are guarded by the modules that own them. - Layout:
{ appLayout: 'admin' }on<base>and<base>/**(the dashboard atbaseitself is not matched by the wildcard, so it gets its own rule). A rule of yours wins. - Locales: with
@nuxtjs/i18nthe rules are repeated for each/<locale>prefix. - Account menu: unless
dashboardorrolesisfalse, an "Admin" entry is added to theaccount.dropdownmenu, shown only to users holding one of the roles.
Server
None. The module registers no server routes.
App
Navigation groups
The sidebar is three groups, each read from its own menu path. ADMIN_NAV_GROUPS (from
@frigear-nu/admin/utils) lists them in display order:
| Group | Menu path | Put here |
|---|---|---|
overview | admin.nav.overview | The dashboard. |
manage | admin.nav (root) | What staff manage every day: users, events, forms, groups, documents. This is the default group. |
settings | admin.nav.settings | Configuration: OAuth applications, integrations. |
The layout reads all three in one useMenu('admin.nav.*') call and renders them as labelled sections (Overview has no
label; Manage and Settings do). A new feature goes into an existing group; a new group needs a
reason (see UX patterns).
Registries
Modules never import the admin module at runtime. The documented way to add a sidebar entry is a
runtime menu source: a module registers a runtime/menus.ts file, and that
file pushes items into the group's menu path with extendMenuItem. It works whether or not @frigear-nu/admin is
installed, because the sidebar is just a menu.
// module.ts
import { addOptionalMenuSource } from '@frigear-nu/module-kit/utils'
addOptionalMenuSource(nuxt, { file: resolver.resolve('./runtime/menus'), menu: 'admin' })
// runtime/menus.ts
import { defineMenuItem, defineMenuSource, extendMenuItem } from '@frigear-nu/menu/utils'
export default defineMenuSource(() => {
const config = useRuntimeConfig().public.forms
// Manage group (the root of `admin.nav`)
extendMenuItem('admin.nav', defineMenuItem({
id: 'forms-admin',
label: 'Forms',
labelKey: 'forms.menu.admin.label',
icon: 'i-lucide-clipboard-list',
to: config.pages.admin,
order: 30,
}))
// Settings group
extendMenuItem('admin.nav.settings', defineMenuItem({
id: 'forms-settings',
label: 'Form settings',
icon: 'i-lucide-settings-2',
to: '/admin/forms/settings',
}))
})
For static entries known at build time there is sugar over extendMenu, plus the dashboard helpers. They come from
@frigear-nu/admin/utils:
import { extendAdminNav, extendAdminDashboard, adminRoute } from '@frigear-nu/admin/utils'
extendAdminNav(nuxt, { label: 'Forms', to: adminRoute(nuxt, 'forms'), icon: 'i-lucide-clipboard-list', order: 20 })
extendAdminNav(nuxt, { label: 'OAuth applications', to: adminRoute(nuxt, 'oauth'), icon: 'i-lucide-key-round' }, 'settings')
extendAdminDashboard(nuxt, { component: resolver.resolve('./runtime/app/widgets/Submissions.vue'), order: 10 })
extendAdminDashboard(nuxt, { component: resolver.resolve('./runtime/app/widgets/SubmissionCount.vue'), order: 10 }, 'top')
| Export | Signature | Purpose |
|---|---|---|
extendAdminNav | (nuxt: Nuxt, items: MenuItem | MenuItem[], group?: 'overview' | 'manage' | 'settings') => void | Adds sidebar entries at build time: extendMenu(nuxt, '<group path>', ...). group defaults to 'manage'. |
extendAdminDashboard | (nuxt: Nuxt, widgets: WidgetDefinition | WidgetDefinition[], position?: 'top' | 'main' | 'side') => void | Adds dashboard widgets to one of the three dashboard areas (below); position defaults to 'main'. |
adminRoute | (nuxt: Nuxt, ...segments: string[]) => string | Path below the configured base: adminRoute(nuxt, 'events', ':id') is /admin/events/:id. Use it so defaults follow the host's base. |
ADMIN_NAV_GROUPS | readonly list | The groups in display order, with the menu path each reads (table above). |
ADMIN_NAV_MENU | 'admin.nav' | Menu name of the sidebar (the Manage group). |
ADMIN_DASHBOARD_AREA | 'admin.dashboard' | Widget area of the main dashboard column. |
ADMIN_DASHBOARD_TOP_AREA | 'admin.dashboard.top' | Widget area of the stat row. |
ADMIN_DASHBOARD_SIDE_AREA | 'admin.dashboard.side' | Widget area of the narrow side column. |
A module can also call extendMenu / extendWidgets directly, without depending on this package.
Dashboard areas
The dashboard is three widget areas, so modules fill it without the admin module knowing them:
| Area | position | Layout | Put here |
|---|---|---|---|
admin.dashboard.top | 'top' | Row of up to four cards (span works) | One StatCard per widget: a number with a label and icon. |
admin.dashboard | 'main' | Wide column below the stats (full width when the side column is empty) | Lists and tables, e.g. ListCard. |
admin.dashboard.side | 'side' | Narrow column next to it, stacked | Short ListCards. |
Widgets load their own data (useLazyFetch, so the page doesn't wait) and receive no context. Without widgets in any
of the areas, the dashboard shows a shortcut card for every entry of the Manage and Settings groups.
Runtime config
runtimeConfig.public.admin holds the resolved values:
type PublicModuleOptions = {
base: string
dashboard?: string // undefined when the dashboard is off
}
Layout and page
| Name | Purpose |
|---|---|
admin layout | A collapsible, resizable sidebar and a navbar over the page body. |
| Dashboard page | admin-dashboard at the dashboard path: a PageHeading and the three widget areas above. Skipped if a page already exists at that path. |
The admin layout is a Nuxt UI dashboard shell:
- Sidebar: the brand, the ⌘K search button, the grouped navigation (Overview, Manage, Settings) and, in the
footer, a "Back to the site" link and the account menu (
AccountMenufrom Account, when installed: it shows the signed-in user and a colour-mode switch under Appearance). - Navbar: the sidebar collapse button, the title of the current section and, with more than one locale,
LanguageSelect. There is no colour-mode button in the navbar: appearance lives in the account menu. - ⌘K: the palette has one group per navigation group, listing the same entries, so everything in the sidebar is
searchable.
AccountMenuandLanguageSelectare resolved by name, so a host's own versions win and neither is required. - Links are built with
useNavLinks, so they keep the visitor's locale prefix and the active entry is highlighted.
Components
The admin pages are built from the shared components of @frigear-nu/ui: PageHeading, PageToolbar,
TableCard, RowMenu, SectionCard, DescriptionList, StatCard, ListCard and FormSlideover. They are registered
globally by that module, which this module now depends on. Use them directly; see UX patterns
for how a list, a detail page and a form are put together.
Admin* components from earlier versions are deprecated and kept for one release as thin aliases over the
shared components, so existing pages keep rendering. Move to the shared components; the aliases will be removed in the
next admin minor.| Deprecated alias | Use instead | Notes |
|---|---|---|
<AdminPage> | <div class="flex flex-col gap-4"> | There is no page wrapper any more. |
<AdminPageHeader> | <PageHeading> | back: { label, to } becomes breadcrumb (a string, or BreadcrumbItem[] ending at the current page); the links slot is actions. |
<AdminToolbar> | <PageToolbar> | Same default slot; adds a right slot. |
<AdminTable> | <TableCard> around <UTable :ui="tableUi()"> | Row click is @select on UTable; the empty shortcut is a UEmpty in the table's #empty slot. |
<AdminSection> | <SectionCard> | Same slots; flush is now a prop. Adds icon. |
<AdminEmpty> | <UEmpty variant="naked"> | Name the thing and offer exactly one action. |
<AdminKeyValue> | <DescriptionList> | Missing values read "Not set" instead of an en dash. |
<AdminStatCard> | <StatCard> | Adds value and hint props; the default slot still overrides the value. |
<AdminListCard> | <ListCard> | toLabel defaults to the translated "View all". |
Modules that ship admin pages declare @frigear-nu/admin as an optional module dependency and
@frigear-nu/ui as a dependency; a host that enables those pages installs both.
Page conventions
Every admin page follows UX patterns. In short:
- Lists:
PageHeadingwith the primary action,PageToolbarfor search past about eight rows,UTableinTableCardwith clickable rows and aRowMenuin the last column. - Detail pages:
PageHeadingwith abreadcrumbback to the list, thenSectionCards; extensible tabs areWidgetTabs, the page's own sections arePageTabs. - Create and edit:
FormSlideoveropened withuseOverlay(), behind ause<Entity>Actions()composable. - Destructive actions:
useConfirmDialog({ destructive: true }), saying what else is affected. - Feedback: a toast with the record's name; errors through
useErrorToast. - Text:
useModuleTwith an English default; buttons name the object ("Add user", not "Add").
Translations
Namespace admin, shipped in English and Danish.
| Key | Used for |
|---|---|
admin.title | Header title when no nav entry matches the route |
admin.nav.label | Sidebar navigation label |
admin.nav.groups.manage, .settings | Headers of the Manage and Settings groups in the sidebar |
admin.search.goTo | Title of the ⌘K palette groups |
admin.dashboard.title, .description, .empty | Dashboard page |
admin.backToSite | Link back to the public site |
admin.menu.label, .description | The "Admin" entry in the account dropdown |
Account
The account area - a layout with its own navigation, an overview and the profile, security, documents and API tokens pages - built on Users and Auth.
Auth
Sign-in (password, magic link, OAuth, passkeys), user API tokens and one requireAuth for cookie sessions, tokens and OAuth access tokens, with scopes.