Skip to content

Last updated:

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

PropTypeDefaultDescription
variant'primary' | 'secondary' | 'outline' | 'ghost' | 'danger''primary'Visual style
size'sm' | 'md' | 'lg''md'Button size
type'button' | 'submit' | 'reset''button'HTML button type
disabledbooleanfalseDisabled state
loadingbooleanfalseShows spinner and disables button
leftIconComponentundefinedIcon component rendered before slot content
rightIconComponentundefinedIcon component rendered after slot content

Slots

SlotDescription
defaultButton label content

Usage

vue
<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

PropTypeDefaultDescription
modelValuestring | number | null''Bound value (v-model)
labelstring''Label text above input
idstringauto-generatedHTML id attribute
type'text' | 'email' | 'password' | 'number' | 'tel' | 'search' | 'date' | 'time''text'Input type
placeholderstring''Placeholder text
disabledbooleanfalseDisabled state
leftIconComponentundefinedIcon inside left of input
rightIconComponentundefinedIcon inside right of input
size'sm' | 'md' | 'lg''md'Input size
fullWidthbooleantrueWhether input fills container width
errorstringundefinedError message displayed below input
requiredbooleanfalseShows red asterisk after label

Events

EventPayloadDescription
update:modelValuestring | number | nullEmitted on input. For type="number", emits number | null.

Usage

vue
<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

PropTypeDefaultDescription
modelValuestring | number | null''Bound value (v-model)
labelstring''Label text
labelIconComponentundefinedIcon next to label
idstringauto-generatedHTML id attribute
placeholderstring''Placeholder text
disabledbooleanfalseDisabled state
rowsnumber | string4Number of visible text rows
size'sm' | 'md' | 'lg''md'Text size
fullWidthbooleantrueWhether textarea fills container width

Usage

vue
<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

PropTypeDefaultDescription
modelValueboolean-- (required)Controls visibility (v-model)
titlestring''Header title text
subtitlestring''Subtitle below title
plainbooleanfalseRemoves 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
showClosebooleantrueShow close button in header
closeOnBackdropbooleantrueClose when clicking backdrop

Events

EventPayloadDescription
update:modelValuebooleanEmitted when modal opens/closes
close--Emitted when modal is closed

Slots

SlotDescription
headerCustom header content (replaces title/subtitle)
defaultModal body content
footerFooter content (hidden when plain is true)

Usage

vue
<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

PropTypeDefaultDescription
columns{ key: string; label: string }[]-- (required)Column definitions
dataRecord<string, unknown>[]-- (required)Row data array
clickablebooleanfalseMakes rows clickable with pointer cursor

Events

EventPayloadDescription
row-clickRecord<string, unknown>Emitted when a clickable row is clicked

Slots

SlotScopeDescription
default{ row, index }Custom row rendering (replaces default <td> cells)
actions{ row, index }Actions column cell content

Usage

vue
<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

PropTypeDefaultDescription
currentPagenumber-- (required)Current page number (v-model)
perPagenumber-- (required)Items per page
totalnumber-- (required)Total number of items

Events

EventPayloadDescription
update:currentPagenumberEmitted when page changes

Usage

vue
<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

ts
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();
MethodSignatureDescription
show(msg: string, type?: 'success' | 'error' | 'info', duration?: number) => voidDisplay a toast
close() => voidDismiss 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.

vue
<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

PropTypeDefaultDescription
titlestringundefinedDialog title
messagestringundefinedConfirmation message
loadingbooleanundefinedShows loading on "Yes" button

Exposed Methods

MethodDescription
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.

ts
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

MethodSignatureDescription
open(opts: ConfirmDialogOptions) => Promise<boolean>Open dialog, returns true if confirmed
confirm() => Promise<void>Programmatic confirm
cancel() => voidProgrammatic cancel
setLoading(value: boolean) => voidControl 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.

PropTypeDefaultDescription
titlestring''Card title
subtitlestring''Subtitle below title
padding'none' | 'sm' | 'md' | 'lg''md'Internal padding

Slots: header, actions, default.

vue
<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.

PropTypeDefaultDescription
type'error' | 'success' | 'warning' | 'info''info'Alert style
titlestring''Bold title
descriptionstring''Alert body text
dismissiblebooleantrueShows close button
vue
<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.

PropTypeDefaultDescription
statusstring-- (required)Status key (case-insensitive)
showIconbooleanfalseShow status icon
vue
<UiBadge status="ACTIVE" show-icon />
<UiBadge status="pending" />

UiPageHeader

Page title bar with subtitle support and an actions slot.

PropTypeDefaultDescription
titlestring-- (required)Page title
subtitlestringundefinedSubtitle text

Slots: subtitle, actions.

vue
<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.

PropTypeDefaultDescription
iconComponent-- (required)Card icon
valuestring | number-- (required)Main stat value
labelstring-- (required)Descriptive label
badgestringundefinedBadge text (e.g., "+12%")
badgeIconComponentundefinedIcon inside badge
color'blue' | 'red' | 'amber' | 'green' | 'primary''primary'Icon background color
badgeColor'green' | 'red' | 'amber' | 'blue' | 'gray''green'Badge color
vue
<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.

PropTypeDefaultDescription
modelValuestring | number | nullnullSelected value (v-model)
fetch(query: string) => Promise<{ label: string; value: string | number }[]>-- (required)Async fetch function
placeholderstring'Select...'Placeholder text
displayLabelstringundefinedInitial display label for pre-selected value
disabledbooleanfalseDisabled state
size'xs' | 'sm''xs'Trigger button size
vue
<UiSearchSelect
  v-model="form.doctor_id"
  :fetch="searchDoctors"
  placeholder="Select a doctor"
  :display-label="selectedDoctorName"
/>

UiSkeletonLoader

Loading placeholder with shimmer animation.

PropTypeDefaultDescription
widthstring | number'100%'Element width
heightstring | number'20px'Element height
variant'rectangle' | 'circle' | 'text''rectangle'Shape variant
shimmerbooleantrueEnable shimmer animation
vue
<UiSkeletonLoader width="200px" height="40px" />
<UiSkeletonLoader variant="circle" width="48" height="48" />
<UiSkeletonLoader variant="text" />

CPR - Clinical Patient Records