Naming Conventions
Consistent naming makes the CPR codebase navigable and predictable. Follow these conventions for all new code.
Files and Directories
Components -- PascalCase.vue
Component files use PascalCase. Domain-specific components are grouped in directories matching their domain.
app/components/
ui/
UiButton.vue
UiCard.vue
UiModal.vue
UiPagination.vue
UiSearchInput.vue
UiTable.vue
UiSnackBar.vue
configuration/
doctor/
DoctorTable.vue
DoctorTableRow.vue
DoctorFilters.vue
forms/
DoctorForms/
DoctorForm.vue
UpdateDoctorForm.vue
patient/
AddPatientModal.vue
PatientMedicalRecord/
PatientIntro.vue
PatientVisits.vue
queue/
QueueList.vue
QueueDetailsPanel.vueComposables -- camelCase.ts
Composable files are named use[Feature].ts. Domain composables live in a subdirectory.
app/composables/
useApiFetch.ts
useFormValidation.ts
useValidationRules.ts
useListFilters.ts
useDialog.ts
useSnackBar.ts
useConfirmDialog.ts
useCanAccess.ts
doctor/
useDoctor.ts
useCreateDoctor.ts
useUpdateDoctor.ts
useDeleteDoctor.tsServices -- [domain].service.ts
Service files use the domain name in camelCase followed by .service.ts. They live in app/sevices/.
app/sevices/
basicCrud.service.ts
doctor.service.ts
patient.service.ts
insurance.service.ts
transaction.service.tsStores -- [domain].store.ts
Store files use the domain name in camelCase followed by .store.ts.
app/stores/
auth.store.ts
doctor.store.ts
patient.store.ts
insurance.store.ts
enums.store.tsTypes -- [domain].type.ts
Type definition files use the domain name in camelCase followed by .type.ts.
app/types/
doctor.type.ts
patient.type.ts
apiReponse.type.ts
permission.type.ts
user.type.tsPages -- kebab-case Directories
Page directories use kebab-case. The index file in each directory is the list/landing page.
app/pages/
login.vue
dashboard/
index.vue
configuration/
doctors/
index.vue
billing-items/
index.vue
surgery-locations/
index.vue
pharmacy/
stock-movement/
index.vue
purchase-orders/
index.vue
reset-password/
index.vue
enter-code.vue
new-password.vue
success-state.vueComponent Names
Domain-Prefixed Components
Components are named with their domain as a prefix for clarity in templates and file trees.
DoctorTable.vue # not Table.vue
DoctorFilters.vue # not Filters.vue
DoctorTableRow.vue # not TableRow.vue
PatientIntro.vue # not Intro.vue
QueueDetailsPanel.vue # not DetailsPanel.vueUI Components -- Ui Prefix
All reusable, domain-agnostic UI components use the Ui prefix. They live in app/components/ui/.
UiButton.vue
UiCard.vue
UiModal.vue
UiTable.vue
UiInput.vue
UiDropdown.vue
UiBadge.vue
UiAlert.vue
UiAvatar.vue
UiPagination.vue
UiSearchInput.vue
UiSearchFilters.vue
UiStatusTabs.vue
UiStatCard.vue
UiSkeletonLoader.vue
UiIconButton.vue
UiFilterButton.vue
UiConfirmDialog.vue
UiSnackBar.vueUtility Components
Cross-cutting components that are not strictly UI primitives live in app/components/utils/.
BranchSelector.vue
PhilippineAddressForm.vueTypeScript Naming
Interfaces and Types
Use PascalCase. Name interfaces after the domain entity they represent.
export interface Doctor {
id: number;
first_name: string;
last_name: string;
full_name: string;
doctor_role_id: number;
doctor_role?: DoctorRole;
status: string;
}
export interface DoctorFilters {
search?: string;
status?: string;
doctor_role_id?: number;
page?: number;
perPage?: number;
}Props Interface
Always name the props interface Props within a component. It is local to the file.
<script setup lang="ts">
interface Props {
variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
loading?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
variant: 'primary',
size: 'md',
});
</script>Pinia Store Functions
Store definitions use use[Domain]Store as the exported function name.
export const useDoctorStore = defineStore('doctor', () => { ... });
export const useAuthStore = defineStore('auth', () => { ... });
export const useEnumsStore = defineStore('enums', () => { ... });Service Exports
Service classes are PascalCase. Export a singleton instance in camelCase and as the default export.
class DoctorService {
// ...
}
export const doctorService = new DoctorService();
export default doctorService;Events
Emit Names -- kebab-case
Use kebab-case for custom events. Follow Vue conventions for v-model events.
defineEmits<{
(e: 'edit', doctor: Doctor): void;
(e: 'delete', doctor: Doctor): void;
(e: 'page-change', page: number): void;
(e: 'row-click', row: Record<string, unknown>): void;
(e: 'update:modelValue', value: string): void;
(e: 'update:searchQuery', value: string): void;
}>();In templates, emit using kebab-case names:
<DoctorTableRow
@edit="$emit('edit', $event)"
@delete="$emit('delete', $event)"
/>Reactive Variables
Boolean Naming
Prefix boolean refs and props with is, has, or show to make their purpose clear.
const isLoading = ref(false);
const isAuthenticated = computed(() => !!state.token);
const showModal = ref(false);
const hasPermission = can('doctors.create');Exception: when the boolean comes from a composable return value, loading, success, and visible are acceptable without prefix for brevity.
// Composable return -- no prefix needed
return { loading, error, success, createDoctor };CSS
Tailwind Utility Classes
Use Tailwind utility classes directly in templates. Avoid custom CSS unless creating reusable component styles.
<template>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<UiInput v-model="form.first_name" label="First Name" />
</div>
</template>Reusable Styles with @apply
When a combination of utilities is repeated across many components, extract it using @apply inside @layer components in your global CSS.
@layer components {
.card-header {
@apply px-6 py-4 border-b border-gray-100 font-semibold text-gray-900;
}
}Constants
UPPER_SNAKE_CASE
True constants (values known at build time, never changing at runtime) use UPPER_SNAKE_CASE.
const MAX_FILE_SIZE = 5 * 1024 * 1024;
const DEFAULT_PER_PAGE = 15;
const API_TIMEOUT_MS = 30000;Configuration objects that are constant use a descriptive camelCase name:
const variantClasses = {
primary: 'text-white bg-[#117ea7] hover:bg-[#0e6b8f] ...',
secondary: 'text-gray-700 bg-white border border-gray-300 ...',
danger: 'text-white bg-red-600 hover:bg-red-700 ...',
};Icons
@heroicons/vue
Import icons from @heroicons/vue/24/outline or @heroicons/vue/24/solid. Use them as components.
<script setup lang="ts">
import { PlusIcon, ArchiveBoxIcon } from '@heroicons/vue/24/outline';
</script>
<template>
<UiButton :left-icon="PlusIcon">Add Doctor</UiButton>
</template>