Base Components
All UI components live in app/components/ui/ and are auto-imported. They are stateless, generic building blocks that accept configuration through props and communicate back through events and slots.
UiButton
Flexible button with variant styling, size options, loading state, and icon support.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
variant | 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger' | 'primary' | Visual style |
size | 'sm' | 'md' | 'lg' | 'md' | Button size |
type | 'button' | 'submit' | 'reset' | 'button' | HTML button type |
disabled | boolean | false | Disabled state |
loading | boolean | false | Shows spinner and disables button |
leftIcon | Component | undefined | Icon component rendered before slot content |
rightIcon | Component | undefined | Icon component rendered after slot content |
Slots
| Slot | Description |
|---|---|
default | Button label content |
Usage
<template>
<!-- Primary submit button with loading -->
<UiButton variant="primary" type="submit" :loading="isSubmitting">
Save Patient
</UiButton>
<!-- Secondary cancel button -->
<UiButton variant="secondary" @click="emit('close')">
Cancel
</UiButton>
<!-- Danger button for delete actions -->
<UiButton variant="danger" :loading="deleting" @click="handleDelete">
Delete
</UiButton>
<!-- Button with icon -->
<UiButton :left-icon="PlusIcon" @click="openForm">
Add Insurance
</UiButton>
<!-- Small ghost button -->
<UiButton variant="ghost" size="sm">
View Details
</UiButton>
</template>UiInput
Text input field with label, error display, icon support, and size variants. Supports v-model binding.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
modelValue | string | number | null | '' | Bound value (v-model) |
label | string | '' | Label text above input |
id | string | auto-generated | HTML id attribute |
type | 'text' | 'email' | 'password' | 'number' | 'tel' | 'search' | 'date' | 'time' | 'text' | Input type |
placeholder | string | '' | Placeholder text |
disabled | boolean | false | Disabled state |
leftIcon | Component | undefined | Icon inside left of input |
rightIcon | Component | undefined | Icon inside right of input |
size | 'sm' | 'md' | 'lg' | 'md' | Input size |
fullWidth | boolean | true | Whether input fills container width |
error | string | undefined | Error message displayed below input |
required | boolean | false | Shows red asterisk after label |
Events
| Event | Payload | Description |
|---|---|---|
update:modelValue | string | number | null | Emitted on input. For type="number", emits number | null. |
Usage
<template>
<UiInput
v-model="form.provider_name"
label="Provider Name"
placeholder="e.g., Maxicare"
:error="errors.provider_name"
required
/>
<UiInput
v-model="form.coverage"
type="number"
label="Coverage (%)"
placeholder="0-100"
:error="errors.coverage"
/>
<UiInput
v-model="form.email"
type="email"
label="Email Address"
:left-icon="EnvelopeIcon"
/>
</template>UiTextArea
Multi-line text input with label and size variants. Supports v-model binding.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
modelValue | string | number | null | '' | Bound value (v-model) |
label | string | '' | Label text |
labelIcon | Component | undefined | Icon next to label |
id | string | auto-generated | HTML id attribute |
placeholder | string | '' | Placeholder text |
disabled | boolean | false | Disabled state |
rows | number | string | 4 | Number of visible text rows |
size | 'sm' | 'md' | 'lg' | 'md' | Text size |
fullWidth | boolean | true | Whether textarea fills container width |
Usage
<template>
<UiTextArea
v-model="form.contact_address"
label="Contact Address"
placeholder="Enter complete address"
rows="2"
/>
</template>UiModal
Teleported modal dialog with header, body, and footer sections. Supports escape-to-close and backdrop click.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
modelValue | boolean | -- (required) | Controls visibility (v-model) |
title | string | '' | Header title text |
subtitle | string | '' | Subtitle below title |
plain | boolean | false | Removes header/footer chrome and padding |
size | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl' | '6xl' | '7xl' | 'full' | 'md' | Modal max-width |
position | 'center' | 'top' | 'center' | Vertical positioning |
showClose | boolean | true | Show close button in header |
closeOnBackdrop | boolean | true | Close when clicking backdrop |
Events
| Event | Payload | Description |
|---|---|---|
update:modelValue | boolean | Emitted when modal opens/closes |
close | -- | Emitted when modal is closed |
Slots
| Slot | Description |
|---|---|
header | Custom header content (replaces title/subtitle) |
default | Modal body content |
footer | Footer content (hidden when plain is true) |
Usage
<template>
<UiModal v-model="showModal" title="Add Patient" size="xl">
<PatientForm @close="showModal = false" />
<template #footer>
<div class="flex justify-end gap-3">
<UiButton variant="secondary" @click="showModal = false">Cancel</UiButton>
<UiButton variant="primary" @click="save">Save</UiButton>
</div>
</template>
</UiModal>
</template>
<script setup lang="ts">
const showModal = ref(false);
</script>TIP
For most forms, prefer useDialog over manual UiModal usage. The UiDialogProvider in the default layout handles rendering automatically. See Forms for details.
UiTable
Data table with column configuration, customizable row rendering, and an actions column.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
columns | { key: string; label: string }[] | -- (required) | Column definitions |
data | Record<string, unknown>[] | -- (required) | Row data array |
clickable | boolean | false | Makes rows clickable with pointer cursor |
Events
| Event | Payload | Description |
|---|---|---|
row-click | Record<string, unknown> | Emitted when a clickable row is clicked |
Slots
| Slot | Scope | Description |
|---|---|---|
default | { row, index } | Custom row rendering (replaces default <td> cells) |
actions | { row, index } | Actions column cell content |
Usage
<template>
<UiTable
:columns="[
{ key: 'provider_name', label: 'Provider' },
{ key: 'plan_type', label: 'Plan Type' },
{ key: 'coverage', label: 'Coverage' },
{ key: 'status', label: 'Status' },
]"
:data="insurances"
clickable
@row-click="viewInsurance"
>
<template #default="{ row }">
<td class="px-6 py-4 text-sm text-gray-600">{{ row.provider_name }}</td>
<td class="px-6 py-4 text-sm text-gray-600">{{ row.plan_type }}</td>
<td class="px-6 py-4 text-sm text-gray-600">{{ row.coverage }}%</td>
<td class="px-6 py-4">
<UiBadge :status="row.status" />
</td>
</template>
<template #actions="{ row }">
<UiIconButton :icon="PencilIcon" @click.stop="editInsurance(row)" />
</template>
</UiTable>
</template>UiPagination
Page navigation component with Previous/Next buttons and entry count display.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
currentPage | number | -- (required) | Current page number (v-model) |
perPage | number | -- (required) | Items per page |
total | number | -- (required) | Total number of items |
Events
| Event | Payload | Description |
|---|---|---|
update:currentPage | number | Emitted when page changes |
Usage
<template>
<UiPagination
v-model:current-page="currentPage"
:per-page="15"
:total="totalInsurances"
/>
</template>
<script setup lang="ts">
const currentPage = ref(1);
</script>The component computes totalPages, from, and to internally and displays "Showing X to Y of Z entries".
UiSnackBar
Global toast notification component. Rendered once in the layout -- you interact with it through the useSnackBar composable.
Composable API: useSnackBar
const snackbar = useSnackBar();
// Show a success message (default type, auto-closes in 3s)
snackbar.show('Insurance created successfully');
// Show an error message
snackbar.show('Failed to save', 'error');
// Show an info message with custom duration
snackbar.show('Processing...', 'info', 5000);
// Manually close
snackbar.close();| Method | Signature | Description |
|---|---|---|
show | (msg: string, type?: 'success' | 'error' | 'info', duration?: number) => void | Display a toast |
close | () => void | Dismiss the current toast |
The snackbar renders at the bottom-right of the viewport with color coding: green for success, red for error, blue for info.
UiConfirmDialog and UiConfirmDialogProvider
Two approaches to confirmation dialogs.
Approach 1: Template Ref (UiConfirmDialog)
Use when you need a standalone confirm dialog in a specific component.
<template>
<UiConfirmDialog
ref="confirmRef"
title="Delete Insurance"
message="Are you sure you want to delete this insurance provider?"
:loading="deleting"
@confirm="handleDelete"
@cancel="confirmRef?.close()"
/>
</template>
<script setup lang="ts">
const confirmRef = ref<{ open: () => void; close: () => void }>();
const openDeleteConfirm = () => {
confirmRef.value?.open();
};
</script>Props
| Prop | Type | Default | Description |
|---|---|---|---|
title | string | undefined | Dialog title |
message | string | undefined | Confirmation message |
loading | boolean | undefined | Shows loading on "Yes" button |
Exposed Methods
| Method | Description |
|---|---|
open() | Show the dialog |
close() | Hide the dialog |
Approach 2: Composable (UiConfirmDialogProvider + useConfirmDialog)
The provider is rendered once in the default layout. Use the composable from any page or component.
const confirm = useConfirmDialog();
const handleDelete = async (id: number) => {
await confirm.open({
title: 'Delete Insurance',
message: 'Are you sure you want to delete this provider?',
onConfirm: async () => {
await deleteInsurance(id);
snackbar.show('Insurance deleted successfully');
refetchCurrentPage();
},
});
};useConfirmDialog API
| Method | Signature | Description |
|---|---|---|
open | (opts: ConfirmDialogOptions) => Promise<boolean> | Open dialog, returns true if confirmed |
confirm | () => Promise<void> | Programmatic confirm |
cancel | () => void | Programmatic cancel |
setLoading | (value: boolean) => void | Control loading state |
The onConfirm callback in options is awaited, and the dialog shows a loading state during execution.
UiDialogProvider
Dynamic dialog renderer used by the useDialog composable. Placed once in the default layout. See Forms -- Using useDialog for usage.
Other UI Components
UiCard
Content card wrapper with optional header, subtitle, and action slot.
| Prop | Type | Default | Description |
|---|---|---|---|
title | string | '' | Card title |
subtitle | string | '' | Subtitle below title |
padding | 'none' | 'sm' | 'md' | 'lg' | 'md' | Internal padding |
Slots: header, actions, default.
<UiCard title="Patient Summary" subtitle="Overview of recent visits">
<template #actions>
<UiButton size="sm" variant="ghost">Export</UiButton>
</template>
<!-- card content -->
</UiCard>UiAlert
Alert banner with type-based styling and optional dismiss.
| Prop | Type | Default | Description |
|---|---|---|---|
type | 'error' | 'success' | 'warning' | 'info' | 'info' | Alert style |
title | string | '' | Bold title |
description | string | '' | Alert body text |
dismissible | boolean | true | Shows close button |
<UiAlert type="error" title="Validation Error" description="Please fix the errors below." />UiBadge
Status badge with a built-in status-to-color map. Supports statuses like ACTIVE, PENDING, COMPLETED, CANCELLED, INSTOCK, LOWSTOCK, OUTOFSTOCK, and many more.
| Prop | Type | Default | Description |
|---|---|---|---|
status | string | -- (required) | Status key (case-insensitive) |
showIcon | boolean | false | Show status icon |
<UiBadge status="ACTIVE" show-icon />
<UiBadge status="pending" />UiPageHeader
Page title bar with subtitle support and an actions slot.
| Prop | Type | Default | Description |
|---|---|---|---|
title | string | -- (required) | Page title |
subtitle | string | undefined | Subtitle text |
Slots: subtitle, actions.
<UiPageHeader title="Insurance Providers" subtitle="Manage insurance provider records">
<template #actions>
<UiButton :left-icon="PlusIcon" @click="openCreateForm">Add Insurance</UiButton>
</template>
</UiPageHeader>UiStatCard
Dashboard stat card with icon, value, label, and optional badge.
| Prop | Type | Default | Description |
|---|---|---|---|
icon | Component | -- (required) | Card icon |
value | string | number | -- (required) | Main stat value |
label | string | -- (required) | Descriptive label |
badge | string | undefined | Badge text (e.g., "+12%") |
badgeIcon | Component | undefined | Icon inside badge |
color | 'blue' | 'red' | 'amber' | 'green' | 'primary' | 'primary' | Icon background color |
badgeColor | 'green' | 'red' | 'amber' | 'blue' | 'gray' | 'green' | Badge color |
<UiStatCard
:icon="UsersIcon"
:value="totalPatients"
label="Total Patients"
badge="+5%"
color="blue"
badge-color="green"
/>UiSearchSelect
Async searchable select dropdown. Fetches options via a provided function.
| Prop | Type | Default | Description |
|---|---|---|---|
modelValue | string | number | null | null | Selected value (v-model) |
fetch | (query: string) => Promise<{ label: string; value: string | number }[]> | -- (required) | Async fetch function |
placeholder | string | 'Select...' | Placeholder text |
displayLabel | string | undefined | Initial display label for pre-selected value |
disabled | boolean | false | Disabled state |
size | 'xs' | 'sm' | 'xs' | Trigger button size |
<UiSearchSelect
v-model="form.doctor_id"
:fetch="searchDoctors"
placeholder="Select a doctor"
:display-label="selectedDoctorName"
/>UiSkeletonLoader
Loading placeholder with shimmer animation.
| Prop | Type | Default | Description |
|---|---|---|---|
width | string | number | '100%' | Element width |
height | string | number | '20px' | Element height |
variant | 'rectangle' | 'circle' | 'text' | 'rectangle' | Shape variant |
shimmer | boolean | true | Enable shimmer animation |
<UiSkeletonLoader width="200px" height="40px" />
<UiSkeletonLoader variant="circle" width="48" height="48" />
<UiSkeletonLoader variant="text" />