Modules

Admin

Generic admin area - guarded shell, dashboard and navigation that other modules contribute to.

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.

Requires@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.

OptionTypeDefaultPurpose
basestring'/admin'Path prefix of the area. A trailing slash is trimmed.
dashboardboolean | stringtrueThe dashboard page at base. false turns it off (other modules' pages still get the layout); a string is a custom path.
rolesstring[] | false['admin']Roles allowed into the area, applied as a route rule on <base>/**. false sets no rule: guard the area yourself.
layoutbooleantrueGives 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/authorization is 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 at base itself is not matched by the wildcard, so it gets its own rule). A rule of yours wins.
  • Locales: with @nuxtjs/i18n the rules are repeated for each /<locale> prefix.
  • Account menu: unless dashboard or roles is false, an "Admin" entry is added to the account.dropdown menu, shown only to users holding one of the roles.

Server

None. The module registers no server routes.

App

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:

GroupMenu pathPut here
overviewadmin.nav.overviewThe dashboard.
manageadmin.nav (root)What staff manage every day: users, events, forms, groups, documents. This is the default group.
settingsadmin.nav.settingsConfiguration: 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')
ExportSignaturePurpose
extendAdminNav(nuxt: Nuxt, items: MenuItem | MenuItem[], group?: 'overview' | 'manage' | 'settings') => voidAdds sidebar entries at build time: extendMenu(nuxt, '<group path>', ...). group defaults to 'manage'.
extendAdminDashboard(nuxt: Nuxt, widgets: WidgetDefinition | WidgetDefinition[], position?: 'top' | 'main' | 'side') => voidAdds dashboard widgets to one of the three dashboard areas (below); position defaults to 'main'.
adminRoute(nuxt: Nuxt, ...segments: string[]) => stringPath below the configured base: adminRoute(nuxt, 'events', ':id') is /admin/events/:id. Use it so defaults follow the host's base.
ADMIN_NAV_GROUPSreadonly listThe 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:

AreapositionLayoutPut 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, stackedShort 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

NamePurpose
admin layoutA collapsible, resizable sidebar and a navbar over the page body.
Dashboard pageadmin-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 (AccountMenu from 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. AccountMenu and LanguageSelect are 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.

The 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 aliasUse insteadNotes
<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: PageHeading with the primary action, PageToolbar for search past about eight rows, UTable in TableCard with clickable rows and a RowMenu in the last column.
  • Detail pages: PageHeading with a breadcrumb back to the list, then SectionCards; extensible tabs are WidgetTabs, the page's own sections are PageTabs.
  • Create and edit: FormSlideover opened with useOverlay(), behind a use<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: useModuleT with an English default; buttons name the object ("Add user", not "Add").

Translations

Namespace admin, shipped in English and Danish.

KeyUsed for
admin.titleHeader title when no nav entry matches the route
admin.nav.labelSidebar navigation label
admin.nav.groups.manage, .settingsHeaders of the Manage and Settings groups in the sidebar
admin.search.goToTitle of the ⌘K palette groups
admin.dashboard.title, .description, .emptyDashboard page
admin.backToSiteLink back to the public site
admin.menu.label, .descriptionThe "Admin" entry in the account dropdown
Copyright © 2026