Skip to main content

Overview

OdontologyApp includes a comprehensive component library built with Svelte 5, utilizing modern reactive patterns with $state, $derived, and $bindable. Components are organized by purpose and reusability.

Component Organization

src/lib/components/
├── ui/                  # Generic UI components
│   ├── Button.svelte
│   ├── Modal.svelte
│   ├── DataTable.svelte
│   ├── EmptyState.svelte
│   ├── Sidebar.svelte
│   ├── Topbar.svelte
│   ├── NotificationBell.svelte
│   └── DashboardAlerts.svelte
├── inputs/              # Form input components
│   ├── TextInput.svelte
│   ├── NumberInput.svelte
│   ├── EmailInput.svelte
│   ├── PasswordInput.svelte
│   ├── PhoneInput.svelte
│   ├── DateInput.svelte
│   ├── SelectInput.svelte
│   ├── TextareaInput.svelte
│   ├── FileInput.svelte
│   ├── SearchInput.svelte
│   └── index.js         # Barrel export
├── Odontology/          # Dental-specific components
│   ├── ToothSVG.svelte
│   └── PatientFinance.svelte
├── patients/            # Patient module components
│   └── tabs/
└── Odontogram.svelte    # Main odontogram component

UI Components

Button

Flexible button component with multiple variants and sizes. Location: src/lib/components/ui/Button.svelte (src/lib/components/ui/Button.svelte:1-112) Props:
type ButtonProps = {
  variant?: "primary" | "outline" | "danger" | "ghost" | "icon" | "icon-danger";
  size?: "sm" | "md" | "lg";
  type?: "button" | "submit" | "reset";
  disabled?: boolean;
  className?: string;
  title?: string;
  onclick?: (e: MouseEvent) => void;
  href?: string;  // Renders as <a> tag if provided
  children?: Snippet;
};
Usage:
<script>
  import Button from "$lib/components/ui/Button.svelte";

  function handleClick() {
    console.log("Button clicked!");
  }
</script>

<!-- Primary button -->
<Button variant="primary" onclick={handleClick}>
  Save Patient
</Button>

<!-- Danger button -->
<Button variant="danger" size="sm">
  Delete
</Button>

<!-- Outline button -->
<Button variant="outline">
  Cancel
</Button>

<!-- Link button -->
<Button href="/patients" variant="outline">
  View Patients
</Button>

<!-- Icon button -->
<Button variant="icon" title="Edit">
  ✏️
</Button>
Variants:
  • primary: Teal background, white text
  • outline: Transparent with border
  • danger: Red background for destructive actions
  • ghost: Transparent, minimal styling
  • icon: Square button for icons
  • icon-danger: Icon button with red styling
Generic modal/dialog component. Location: src/lib/components/ui/Modal.svelte Props:
type ModalProps = {
  show: boolean;           // Controls visibility
  title?: string;          // Modal header
  onclose?: () => void;    // Close callback
  children?: Snippet;      // Modal content
};
Usage:
<script>
  import Modal from "$lib/components/ui/Modal.svelte";
  
  let showModal = $state(false);
</script>

<Button onclick={() => showModal = true}>Open Modal</Button>

<Modal show={showModal} title="Edit Patient" onclose={() => showModal = false}>
  <form>
    <!-- Form fields -->
  </form>
</Modal>

DataTable

Powerful data table with pagination, sorting, and custom rendering. Location: src/lib/components/ui/DataTable.svelte (src/lib/components/ui/DataTable.svelte:1-50) Props:
type DataTableProps = {
  data?: any[];                    // Array of records
  columns?: string[];              // Column headers
  loading?: boolean;               // Loading state
  emptyMessage?: string;           // No data message
  row?: Snippet<[any]>;           // Custom row renderer
  grid?: Snippet<[any]>;          // Grid view renderer
  footer?: Snippet;               // Footer content
  toolbar?: Snippet;              // Toolbar above table
  emptyState?: Snippet;           // Custom empty state
  className?: string;             // Additional CSS classes
};
Built-in features:
  • Pagination (10, 25, 50, 75, 100 records per page)
  • Auto-reset to page 1 on data change
  • Loading skeleton
  • Empty state
  • Responsive design
Usage:
<script>
  import DataTable from "$lib/components/ui/DataTable.svelte";
  
  let patients = $state([]);
  let loading = $state(true);
  
  async function loadPatients() {
    loading = true;
    const res = await fetch("/api/patients");
    const data = await res.json();
    patients = data.patients;
    loading = false;
  }
</script>

<DataTable 
  data={patients} 
  {loading}
  columns={["Name", "ID", "Phone", "Actions"]}
  emptyMessage="No patients found"
>
  {#snippet row(patient)}
    <tr>
      <td>{patient.first_name} {patient.last_name}</td>
      <td>{patient.medrecno}</td>
      <td>{patient.phone}</td>
      <td>
        <Button variant="icon" href="/patients/{patient.id}">👁️</Button>
      </td>
    </tr>
  {/snippet}
</DataTable>
Pagination state:
let pageSize = $state(10);         // Records per page
let currentPage = $state(1);       // Current page number
let totalPages = $derived(Math.ceil(data.length / pageSize));
let paginatedData = $derived(data.slice(startIndex, endIndex));

EmptyState

Fallback component when no data is available. Location: src/lib/components/ui/EmptyState.svelte Usage:
<EmptyState 
  icon="📋" 
  message="No patients found" 
  description="Create your first patient to get started."
/>

Input Components

All input components share a consistent API and styling.

Common Props

type InputProps = {
  id: string;              // Unique ID for label association
  label?: string;          // Field label
  value: string;           // Bindable value
  icon?: string;           // Left icon/emoji
  placeholder?: string;    // Placeholder text
  required?: boolean;      // Mark as required
  disabled?: boolean;      // Disable input
  hint?: string;           // Help text
  error?: string;          // Error message
  onchange?: () => void;   // Change handler
  oninput?: () => void;    // Input handler
};

TextInput

Generic text input field. Location: src/lib/components/inputs/TextInput.svelte (src/lib/components/inputs/TextInput.svelte:1-167) Additional Props:
type?: "text" | "email" | "url" | "tel";  // Input type
maxlength?: number;                      // Character limit
Usage:
<script>
  import { TextInput } from "$lib/components/inputs";
  
  let firstName = $state("");
  let lastName = $state("");
  let error = $state("");
</script>

<TextInput 
  id="first_name"
  label="First Name"
  bind:value={firstName}
  icon="👤"
  placeholder="Enter first name"
  required
  error={error}
/>

<TextInput 
  id="last_name"
  label="Last Name"
  bind:value={lastName}
  icon="👤"
  placeholder="Enter last name"
  required
/>
Styling features:
  • Teal left border accent
  • Icon support with proper spacing
  • Focus state with shadow and background
  • Error state with red border
  • Disabled state with gray background

DateInput

Date picker input. Location: src/lib/components/inputs/DateInput.svelte Usage:
<script>
  import { DateInput } from "$lib/components/inputs";
  
  let birthDate = $state("");
</script>

<DateInput 
  id="birth_date"
  label="Date of Birth"
  bind:value={birthDate}
  icon="📅"
  required
/>

SelectInput

Dropdown select input. Location: src/lib/components/inputs/SelectInput.svelte Props:
options: Array<{ value: string, label: string }>;
Usage:
<script>
  import { SelectInput } from "$lib/components/inputs";
  
  let sex = $state("");
  
  const sexOptions = [
    { value: "masculino", label: "Masculino" },
    { value: "femenino", label: "Femenino" },
    { value: "otro", label: "Otro" },
  ];
</script>

<SelectInput 
  id="sex"
  label="Sex"
  bind:value={sex}
  options={sexOptions}
  icon="⚧️"
  required
/>

EmailInput

Email input with validation. Location: src/lib/components/inputs/EmailInput.svelte Usage:
<EmailInput 
  id="email"
  label="Email Address"
  bind:value={email}
  icon="📧"
  placeholder="[email protected]"
/>

PhoneInput

Phone number input. Location: src/lib/components/inputs/PhoneInput.svelte Usage:
<PhoneInput 
  id="phone"
  label="Phone Number"
  bind:value={phone}
  icon="📱"
  placeholder="+56 9 8888 1234"
/>

TextareaInput

Multi-line text input. Location: src/lib/components/inputs/TextareaInput.svelte Additional Props:
rows?: number;  // Number of visible rows
Usage:
<TextareaInput 
  id="allergies"
  label="Known Allergies"
  bind:value={allergies}
  icon="⚠️"
  rows={4}
  placeholder="List any known allergies..."
/>

PasswordInput

Password input with show/hide toggle. Location: src/lib/components/inputs/PasswordInput.svelte Usage:
<PasswordInput 
  id="password"
  label="Password"
  bind:value={password}
  icon="🔒"
  required
/>

FileInput

File upload input. Location: src/lib/components/inputs/FileInput.svelte Props:
accept?: string;  // File type filter (e.g., "image/*")
multiple?: boolean;
Usage:
<FileInput 
  id="xray"
  label="Upload X-Ray"
  accept="image/*"
  hint="Supported formats: JPG, PNG, PDF"
/>

SearchInput

Search input with live filtering. Location: src/lib/components/inputs/SearchInput.svelte Usage:
<script>
  import { SearchInput } from "$lib/components/inputs";
  
  let searchQuery = $state("");
  
  let filteredPatients = $derived(
    patients.filter(p => 
      p.first_name.toLowerCase().includes(searchQuery.toLowerCase())
    )
  );
</script>

<SearchInput 
  id="search"
  bind:value={searchQuery}
  placeholder="Search patients..."
/>

Barrel Export

All input components are exported from a single index file: Location: src/lib/components/inputs/index.js
export { default as TextInput } from "./TextInput.svelte";
export { default as NumberInput } from "./NumberInput.svelte";
export { default as EmailInput } from "./EmailInput.svelte";
export { default as PasswordInput } from "./PasswordInput.svelte";
export { default as PhoneInput } from "./PhoneInput.svelte";
export { default as DateInput } from "./DateInput.svelte";
export { default as SelectInput } from "./SelectInput.svelte";
export { default as TextareaInput } from "./TextareaInput.svelte";
export { default as FileInput } from "./FileInput.svelte";
export { default as SearchInput } from "./SearchInput.svelte";
Import all at once:
<script>
  import { 
    TextInput, 
    EmailInput, 
    DateInput, 
    SelectInput 
  } from "$lib/components/inputs";
</script>

Dental Components

Odontogram

Interactive dental chart for marking tooth conditions. Location: src/lib/components/Odontogram.svelte (src/lib/components/Odontogram.svelte:1-50) Props:
type OdontogramProps = {
  toothStates: Record<number, string>;  // Bindable state object
  readOnly?: boolean;                   // Disable editing
  onsubmitted?: () => void;             // Save callback
};
Tooth numbering: FDI notation (11-18, 21-28, 31-38, 41-48) State values:
  • "" - Healthy/normal
  • "caries" - Tooth decay
  • "restored" - Filled/restored
  • "sealant" - Dental sealant
  • "fracture" - Broken tooth
  • "extracted" - Missing tooth
  • "crown" - Crown/cap
  • "treatment" - Under treatment
  • "endodontics" - Root canal
Surface markers: V, M, D, O, L (vestibular, mesial, distal, occlusal, lingual) Usage:
<script>
  import Odontogram from "$lib/components/Odontogram.svelte";
  
  let toothStates = $state({
    18: "extracted",
    21: "caries:V,M",  // Caries on vestibular and mesial surfaces
    36: "restored",
  });
  
  async function saveOdontogram() {
    const res = await fetch(`/api/patients/${patientId}/odontogram`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ toothStates }),
    });
    // Handle response
  }
</script>

<Odontogram bind:toothStates onsubmitted={saveOdontogram} />

<!-- Read-only view -->
<Odontogram toothStates={existingStates} readOnly />
Internal structure:
const upperTeeth = [18, 17, 16, 15, 14, 13, 12, 11, 21, 22, 23, 24, 25, 26, 27, 28];
const lowerTeeth = [48, 47, 46, 45, 44, 43, 42, 41, 31, 32, 33, 34, 35, 36, 37, 38];

let showTreatmentModal = $state(false);
let selectedTooth = $state(0);
let selectedSurface = $state("");

let treatmentData = $state({
  generalState: "",
  surfaces: {},  // Surface-specific states
  notes: "",
});

ToothSVG

SVG component for rendering individual teeth with surface markers. Location: src/lib/components/Odontology/ToothSVG.svelte Props:
type ToothSVGProps = {
  toothNumber: number;
  state: string;
  onclick?: () => void;
};
Usage:
<ToothSVG 
  toothNumber={18} 
  state="caries" 
  onclick={() => openToothModal(18)} 
/>

PatientFinance

Budget and payment management for patients. Location: src/lib/components/Odontology/PatientFinance.svelte Features:
  • Budget creation with line items
  • Payment recording
  • Financial history
  • Balance calculation
Usage:
<PatientFinance patientId={patient.id} />

Layout Components

Main navigation sidebar. Location: src/lib/components/ui/Sidebar.svelte Features:
  • Role-based menu items
  • Active route highlighting
  • Collapsible sections
  • Permission-based visibility

Topbar

Top navigation bar with user menu. Location: src/lib/components/ui/Topbar.svelte Features:
  • User profile display
  • Notifications
  • Theme toggle
  • Logout button

NotificationBell

Notification center component. Location: src/lib/components/ui/NotificationBell.svelte Features:
  • Real-time notification badge
  • Dropdown notification list
  • Mark as read functionality

Styling Conventions

CSS Variables

Components use CSS custom properties defined in app.css:
:root {
  --teal: #0d9488;
  --teal-light: #14b8a6;
  --teal-faint: #f0fdfa;
  --red: #ef4444;
  --slate-100: #f1f5f9;
  --slate-200: #e2e8f0;
  --slate-400: #94a3b8;
  --slate-600: #475569;
  --slate-800: #1e293b;
  --white: #ffffff;
}

Component CSS

Components use scoped styles with consistent patterns:
<style>
  .field {
    display: flex;
    flex-direction: column;
    gap: 5px;
  }
  
  .field-label {
    font-family: "DM Sans", sans-serif;
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.5px;
    text-transform: uppercase;
    color: var(--slate-600);
  }
  
  .field-input {
    padding: 9px 13px;
    border: 1.5px solid var(--slate-200);
    border-left: 3px solid var(--teal-light);
    border-radius: 9px;
    font-size: 13.5px;
    transition: border-color 0.18s, box-shadow 0.18s;
  }
  
  .field-input:focus {
    border-color: var(--teal);
    box-shadow: 0 0 0 3px rgba(13, 148, 136, 0.12);
    background: var(--teal-faint);
  }
</style>

Svelte 5 Patterns

Reactive State

// State
let count = $state(0);

// Derived
let doubled = $derived(count * 2);

// Effect
$effect(() => {
  console.log(`Count is now ${count}`);
});

// Bindable prop
let { value = $bindable("") } = $props();

Snippets (Render Props)

<!-- Component definition -->
<script>
  let { row } = $props();
</script>

{#each items as item}
  {@render row(item)}
{/each}

<!-- Usage -->
<DataTable data={patients}>
  {#snippet row(patient)}
    <tr><td>{patient.name}</td></tr>
  {/snippet}
</DataTable>

Build docs developers (and LLMs) love