Layouts
CPR uses Nuxt layouts to provide consistent page chrome -- headers, navigation, providers, and module-specific tab bars. Layouts live in app/layouts/ and are assigned to pages via definePageMeta.
How Layouts Are Assigned
Pages declare their layout using definePageMeta in <script setup>:
<!-- app/pages/login.vue -->
<script setup lang="ts">
definePageMeta({
layout: 'auth',
});
</script>If no layout is specified, Nuxt uses default. Some module layouts (configuration, pharmacy, queue) nest inside the default layout using <NuxtLayout name="default">, so pages using those layouts get the full app chrome plus module-specific tab navigation.
Layout Reference
default
File: app/layouts/default.vue
The primary application layout used by most pages. Provides the full app shell.
What it provides:
- Teal header bar (
bg-[#117ea7]) with the CPR logo - Desktop navigation tabs: Dashboard, Patient, Queue, Surgery Schedule, Pharmacy, Configuration
- Permission-gated navigation -- tabs are only shown if the user has the required permission (e.g.,
patients.access) - Mobile hamburger menu with slide-out navigation
BranchSelectorcomponent for switching clinic branches- User profile dropdown with avatar, role display, profile link, and logout
<UiSnackBar />for toast notifications<UiDialogProvider />for dynamic form modals opened viauseDialog<UiConfirmDialogProvider />for confirmation dialogs opened viauseConfirmDialog
When to use: All authenticated application pages. This is the default -- you don't need to specify it explicitly.
<!-- This page uses the default layout automatically -->
<template>
<div class="px-6 lg:px-8 pb-8">
<UiPageHeader title="Dashboard" />
<!-- page content -->
</div>
</template>WARNING
The default layout renders UiDialogProvider and UiConfirmDialogProvider. These are required for useDialog and useConfirmDialog to work. If you create a custom layout, include these providers.
auth
File: app/layouts/auth.vue
Split-screen layout for unauthenticated pages (login, password reset).
What it provides:
- Left panel (42% width, hidden on mobile): Teal gradient branding with CPR logo, tagline, animated floating particles, rotating inspirational quotes, and app version number
- Right panel: Centered white content area (max-width 480px) with the CPR logo above the slot
- Fully responsive -- on mobile, only the right content panel is shown
When to use: Login, forgot password, and code verification pages.
<!-- app/pages/login.vue -->
<script setup lang="ts">
definePageMeta({
layout: 'auth',
});
</script>
<template>
<div>
<h2 class="text-2xl font-bold mb-6">Sign in to your account</h2>
<!-- login form -->
</div>
</template>configuration
File: app/layouts/configuration.vue
Module layout for all configuration pages. Nests inside the default layout.
What it provides:
- All default layout features (header, nav, providers)
- Horizontal tab bar with permission-gated tabs:
- Medicine (default tab at
/configuration) - Procedures (
/configuration/procedures) - Insurance (
/configuration/insurances) - Billing Items (
/configuration/billing-items) - Doctors (
/configuration/doctors) - Surgery Locations (
/configuration/surgery-locations)
- Medicine (default tab at
- Active tab highlighting based on the current route name
When to use: Pages under /configuration/.
<!-- app/pages/configuration/insurances/index.vue -->
<script setup lang="ts">
definePageMeta({
layout: 'configuration',
});
</script>
<template>
<div class="px-6 lg:px-8 pb-8">
<!-- Insurance list page content -->
</div>
</template>pharmacy
File: app/layouts/pharmacy.vue
Module layout for pharmacy pages. Nests inside the default layout.
What it provides:
- All default layout features
- Horizontal tab bar with permission-gated tabs:
- Transactions (default tab at
/pharmacy) - Inventory (
/pharmacy/inventory) - Stock Movement (
/pharmacy/stock-movement) - Items (
/pharmacy/items) - Purchase Orders (
/pharmacy/purchase-orders) - Deliveries (
/pharmacy/deliveries) - Suppliers (
/pharmacy/suppliers)
- Transactions (default tab at
- Content area with horizontal padding (
px-6 lg:px-8)
When to use: Pages under /pharmacy/.
<script setup lang="ts">
definePageMeta({
layout: 'pharmacy',
});
</script>queue
File: app/layouts/queue.vue
Module layout for queue management pages. Nests inside the default layout.
What it provides:
- All default layout features
- Horizontal tab bar with permission-gated tabs:
- Todays Queue (default tab at
/queue) - Transactions List (
/queue/transactions)
- Todays Queue (default tab at
- Content area with horizontal padding
When to use: Pages under /queue/.
<script setup lang="ts">
definePageMeta({
layout: 'queue',
});
</script>print
File: app/layouts/print.vue
Minimal layout for print-friendly pages. No header, no navigation, no providers.
What it provides:
- Clean white background
- 2rem padding on screen, zero padding when printing
- Print-specific CSS: A4 page size, 1.5cm margins, exact color reproduction
- Small base font size (11px) optimized for printed output
When to use: Report and print preview pages.
<script setup lang="ts">
definePageMeta({
layout: 'print',
});
</script>
<template>
<ReportLayout>
<!-- report content -->
</ReportLayout>
</template>WARNING
The print layout does not include UiDialogProvider, UiConfirmDialogProvider, or UiSnackBar. The useDialog, useConfirmDialog, and useSnackBar composables will not work on print pages.
Module Layout Architecture
The configuration, pharmacy, and queue layouts share a common pattern: they nest inside the default layout and add a tab bar. This creates a two-level navigation structure:
default layout
+-- Header (logo, nav, branch selector, user dropdown)
+-- Providers (snackbar, dialog, confirm dialog)
+-- module layout (configuration / pharmacy / queue)
+-- Tab bar
+-- <slot /> (page content)The tab bar uses NuxtLink components pointed at the module's sub-routes, with active state derived from route.name. Tabs are permission-gated using the $can() helper:
<template v-for="tab in tabs">
<NuxtLink
v-if="$can(tab.permission)"
:key="tab.value"
:to="`/configuration/${tab.value}`"
:class="activeTab === tab.value ? 'active-classes' : 'inactive-classes'"
>
{{ tab.label }}
</NuxtLink>
</template>