[data-reveal]{opacity:1!important;transform:none!important}
Frontend Composables

Page context

Composables exposing the dashboard's nav tree, current module, ownership, and per-user preferences — plus the page setup functions a backend page can name.

These composables let your layer's Vue read where it sits in the dashboard — the navigation tree, the current module, whether the viewer owns the tenant — and persist per-user UX preferences. The state-bearing ones are shared, SSR-safe singletons, so every component sees the same values.

Layout & module context

The dashboard fetches its navigation and module map once into shared state; these composables expose that state and derive the current position from the route.

useSiteLayout()

useSiteLayout exposes the dashboard's loaded structure as a bag of shared reactive refs. Layer authors read these — most often modules, isOwner, and quickActions:

RefTypeHolds
siteLayoutSiteLayout | undefinedflat { pages, categories } keyed by slug
siteLayoutTreeSiteLayoutTree | undefinedthe nested nav tree (children + ordering)
quickActionsQuickActionsPayload | undefined{ categories, actions } for the command palette
modulesRecord<string, ModuleInfo & { hasAccess: boolean }> | undefinedevery registered module by id
isOwnerbooleanviewer owns the current tenant
pageLayoutsRecord<string, PageLayout>per-slug component-layout cache
isLoadingbooleana fetch is in flight
loadingErrorstring | nulllast load error message

siteLayout.categories holds the categories that own a URL. A category declared with urlSlug: "/" adds no segment of its own — it is a section heading, not a route — so it is absent there and reachable through siteLayoutTree, which is keyed by fullId.

app/components/MyWidget.vue
const { modules, isOwner, quickActions } = useSiteLayout();
const myModule = computed(() => modules.value?.["my-module"]);
The same composable also returns loader and route-matching methods — loadSiteLayout, loadPageLayout, refresh, refreshIfStale, findMatchingRoute, findMatchingRouteOrCategory, and the dev-only probeAndCommitRoute. These are internal framework plumbing (the page renderer, middleware, and dev hot-reload plugin call them). Treat the refs above as your read surface and leave the methods alone.

useCurrentModule()

() → ComputedRef<{ id, info } | null> — the module owning the current route, derived from a /modules/<id>/… path and looked up in modules. Returns null off any module page. info is the ModuleInfo & { hasAccess } entry.

const current = useCurrentModule(); // current.value?.id

useIsOwner()

() → ComputedRef<boolean> — convenience wrapper over useSiteLayout().isOwner; true only when the viewer owns the tenant. Use it to gate owner-only UI. See Auth & permissions.

useModulesListing()

() → { modules, isLoading, loadingError, loadModulesListing, refresh } — owner-only fetch of /cms/modules-listing (each entry adds a landingSlug). Call loadModulesListing() to populate modules; a 403 is caught and surfaced as a friendly loadingError ("Forbidden: only owners can list modules") rather than thrown. For the management/marketplace surface, not per-page rendering.

useModuleHistory()

() → { record(moduleId, path), getLast(moduleId), entries } — remembers the last path visited inside each module (in-memory shared state) so navigation can resume where the user left off. getLast returns the stored path or null; entries is a computed list of { moduleId, path } ordered oldest to newest visit — recording an already-known module moves it to the end, so the ordering reflects true recency (the command palette's "recent modules" group reads it reversed).

const history = useModuleHistory();
history.record("crm", "/modules/crm/contacts/42");
const back = history.getLast("crm"); // "/modules/crm/contacts/42"
const recent = history.entries.value; // [{ moduleId: "crm", path: "…" }, …]

useQuickActions()

() → { categoryGroups: ComputedRef<QuickActionCategoryGroup[]> } — the quick actions of the current user (from useSiteLayout().quickActions), grouped by category and sorted by order on both levels. The payload only ever carries actions the caller can run — the server leaves the others out — so there is no access flag to filter on. It is the shared source behind every quick-action surface, the header popover and the command palette, so read it instead of re-deriving groups from the raw payload. The module also exports dispatchQuickActionTarget(target), which runs a quick action's client-side behavior: navigate and openForm route to the action's page (openForm carrying the intent in the query, which the table view picks up when it mounts), and event dispatches a window CustomEvent in place.

const { categoryGroups } = useQuickActions();
// categoryGroups.value → [{ category, actions }, …], permission-filtered
dispatchQuickActionTarget(action.target); // run one

Page setup functions (setupId)

A backend page can name a setup function in its setupId option (Pages & components). The renderer resolves that id from the function registry when the page mounts, so your layer registers the handler once, from a plugin:

app/plugins/page-setups.client.ts
export default defineNuxtPlugin(() => {
  const { registerFunction } = useDefinedFunctions();

  registerFunction("app.pages.DashboardSetup", (context: PageSetupContext) => {
    // React to any component on the page:
    const off = context.on("form", FormEvents.FIELD_CHANGE, (data) => {
      context.emit("chart", "app.chart.Highlight", data);
    });
    return () => off(); // cleanup — runs on page unmount
  });
});

PageSetupContext carries pageInfo (the resolved page), permissions (the viewer's set), and the on / emit pair — a page-scoped view of the Component events. on returns an unsubscribe function; the setup function may return a cleanup callback (or a promise of one), which the renderer calls on unmount.

Use a setup function when a page needs orchestration across components but no custom rendering — it keeps the page fully backend-described.

User preferences & UX

These composables persist per-user settings in cookies, so they survive reloads and are available during SSR.

usePreferences()

usePreferences is a cookie-backed key/value store (user-preferences cookie, one-year max-age) addressed by dotted paths. It returns:

MemberPurpose
preferencesreadonly snapshot of the whole tree
getPreference(keyPath, default?)read a nested value, falling back to default
setPreference(keyPath, value)write a value, creating intermediate objects
updatePreference(keyPath, partial)shallow-merge into an existing object value
removePreference(keyPath)delete a key
clearAllPreferences()reset to {}
const { getPreference, setPreference } = usePreferences();
setPreference("tables.contacts.pageSize", 50);
const size = getPreference("tables.contacts.pageSize", 25);

useInterfaceScale()

() → CookieRef<InterfaceScale> — UI density as a writable cookie ref (cms-interface-scale, values "small" | "normal" | "large", default "normal"). Assign to .value to change it.

useFavoritePages()

useFavoritePages tracks the user's pinned pages, persisted to the cms-favorite-pages cookie and mirrored in shared state. A FavoritePage records the page's id, path, title, and optional icon. It returns:

MemberPurpose
favorites / sortedFavoritesraw list / title-sorted computed list
isFavorite(path)is this path pinned
toggleFavorite(page)add/remove, returns the new pinned state
addFavorite(page) / removeFavorite(path)explicit add / remove
clearFavorites()remove all
cleanupInvalidFavorites(isValidPath)drop entries failing the predicate
const { isFavorite, toggleFavorite } = useFavoritePages();
toggleFavorite({ id: "p1", path: route.path, title: "Contacts", icon: "i-ph-users" });

App config & system

useHomepage()

() → string — the dashboard's home path, from the homepage config key (Configuration), defaulting to "/". Use it for "back to home" links instead of hard-coding /.

useFirstAccessiblePagePath()

() → string | undefined — the path of the first page the current user can access, in tree (sidebar) order. Categories the user cannot access are still descended into (they may hold accessible pages), but only navigable, non-hidden pages the user can access are returned; settings pages are a last resort and module pages are never chosen (they have their own routing). The page renderer uses it to redirect a user whose role cannot access the configured homepage; undefined when the site layout is not loaded or no page is accessible. A pure firstAccessiblePagePath(tree) variant takes the layout tree directly, for use after an await where composable context is unavailable.

useSystemState()

Exposes the instance's system state, fetched server-side so it is already available during SSR. It returns:

  • systemState — the has_onboarded flag (what the onboarding gating reads) and the dashboard meta (title, description) from the backend config.
  • metaTitle / metaDescription — computed conveniences for a page's <head>.
  • error / refresh() — the fetch error, and a refetch to call after a state change.