Helper Components

Helper components for loading, errors, empty states, and more

Helper Components Overview

Utility components provide common functionality like loading states, error handling, empty states, and other helper UI elements. These components are located in src/lib/components/ and are used throughout the application.

Loading Components

LoadingSpinner

Simple loading spinner with customizable size and message.

<script lang="ts">
  import LoadingSpinner from '$lib/components/LoadingSpinner.svelte';
</script>

<!-- Basic usage -->
<LoadingSpinner />

<!-- With custom message -->
<LoadingSpinner message="Loading data..." />

<!-- Different sizes -->
<LoadingSpinner size="sm" message="Loading..." />
<LoadingSpinner size="md" message="Please wait..." />
<LoadingSpinner size="lg" message="Loading content..." />

Props:

  • message: string - Loading message (default: "Loading...")
  • size: 'sm' | 'md' | 'lg' - Spinner size (default: "md")

Size Classes:

  • sm: 8x8 spinner, small text
  • md: 12x12 spinner, base text (default)
  • lg: 16x16 spinner, large text

LoadingScreen

Full-screen loading overlay for page transitions.

<script lang="ts">
  import LoadingScreen from '$lib/components/LoadingScreen.svelte';
  
  let loading = $state(true);
</script>

{#if loading}
  <LoadingScreen message="Loading application..." />
{/if}

Props:

  • message: string - Loading message
  • fullScreen: boolean - Cover entire screen (default: true)

Use Cases:

  • Initial app load
  • Route transitions
  • Authentication checks
  • Critical data loading

Error Components

ErrorBoundary

Error boundary component that catches and displays errors gracefully.

<script lang="ts">
  import ErrorBoundary from '$lib/components/ErrorBoundary.svelte';
  
  let error = $state<Error | null>(null);
</script>

<ErrorBoundary {error}>
  {#snippet children()}
    <!-- Component that might error -->
    <MyComponent />
  {/snippet}
  
  {#snippet fallback(error)}
    <div class="p-4 border border-red-500 rounded-lg">
      <h3 class="text-red-600 font-semibold">Error</h3>
      <p class="text-sm text-red-500">{error.message}</p>
    </div>
  {/snippet}
</ErrorBoundary>

Props:

  • error: Error | null - Error object
  • children: Snippet - Content to wrap
  • fallback: (error: Error) => Snippet - Error display

Features:

  • Catches component errors
  • Displays error message
  • Provides retry option
  • Logs errors for debugging

Empty State Components

EmptyState

Display when there's no data to show.

<script lang="ts">
  import EmptyState from '$lib/components/EmptyState.svelte';
  import { DocumentText, InboxIcon } from 'svelte-hero-icons';
  
  let items = $state<any[]>([]);
</script>

{#if items.length === 0}
  <EmptyState
    title="No items found"
    message="Get started by creating your first item"
    icon={InboxIcon}
    size="md"
  />
{/if}

Props:

  • title: string - Empty state title (required)
  • message: string - Empty state message (required)
  • icon: any - Heroicon component (default: DocumentText)
  • size: 'sm' | 'md' | 'lg' - Size variant (default: "md")

Size Variants:

  • sm: Small icon (6x6), base text
  • md: Medium icon (8x8), large text (default)
  • lg: Large icon (10x10), xl text

Common Use Cases:

  • Empty lists
  • No search results
  • No data available
  • Initial state

EmptyState with Action

<script lang="ts">
  import EmptyState from '$lib/components/EmptyState.svelte';
  import { Button } from '$lib/components/ui';
  import { PlusIcon } from 'svelte-hero-icons';
</script>

<EmptyState
  title="No assessments yet"
  message="Create your first assessment to get started"
  icon={PlusIcon}
>
  {#snippet action()}
    <Button variant="primary" icon={PlusIcon} onclick={() => createAssessment()}>
      Create Assessment
    </Button>
  {/snippet}
</EmptyState>

File Components

FileCard

Display file information with preview and actions.

<script lang="ts">
  import FileCard from '$lib/components/FileCard.svelte';
  
  const file = {
    id: 1,
    name: 'document.pdf',
    size: 1024000,
    type: 'application/pdf',
    uploadedAt: new Date(),
    url: '/files/document.pdf'
  };
</script>

<FileCard {file} />

Props:

  • file: FileInfo - File information object
  • onDownload: () => void - Download callback
  • onDelete: () => void - Delete callback
  • showActions: boolean - Show action buttons

FileInfo Interface:

interface FileInfo {
  id: number | string;
  name: string;
  size: number; // bytes
  type: string; // MIME type
  uploadedAt: Date;
  url: string;
  thumbnailUrl?: string;
}

Features:

  • File type icons
  • Size formatting
  • Preview thumbnails
  • Download/delete actions
  • Upload date display

FileUploadButton

Button component for file uploads with drag-and-drop support.

<script lang="ts">
  import FileUploadButton from '$lib/components/FileUploadButton.svelte';
  
  function handleUpload(files: File[]) {
    console.log('Uploaded files:', files);
    // Upload logic
  }
  
  function handleError(error: Error) {
    console.error('Upload error:', error);
  }
</script>

<FileUploadButton
  accept=".pdf,.doc,.docx"
  multiple={true}
  maxSize={10 * 1024 * 1024} // 10MB
  onUpload={handleUpload}
  onError={handleError}
/>

Props:

  • accept: string - Accepted file types (e.g., ".pdf,.doc")
  • multiple: boolean - Allow multiple files
  • maxSize: number - Maximum file size in bytes
  • onUpload: (files: File[]) => void - Upload callback
  • onError: (error: Error) => void - Error callback
  • disabled: boolean - Disable upload

Features:

  • Drag and drop
  • File type validation
  • Size validation
  • Progress indication
  • Error handling

Toast Components

Toast

Individual toast notification component.

<script lang="ts">
  import Toast from '$lib/components/Toast.svelte';
  
  const toast = {
    id: '1',
    message: 'Operation completed successfully',
    type: 'success',
    duration: 5000
  };
</script>

<Toast {toast} />

Props:

  • toast: ToastData - Toast data object
  • onDismiss: (id: string) => void - Dismiss callback

ToastData Interface:

interface ToastData {
  id: string;
  message: string;
  type: 'success' | 'error' | 'info' | 'warning';
  duration?: number; // milliseconds
}

ToastContainer

Container for managing multiple toast notifications.

<script lang="ts">
  import ToastContainer from '$lib/components/ToastContainer.svelte';
  import { toast } from '$lib/stores/toast';
</script>

<ToastContainer />

<!-- Usage -->
<script>
  toast.success('Saved successfully!');
  toast.error('Failed to save');
  toast.info('New update available');
  toast.warning('Please review your changes');
</script>

Toast Store Methods:

  • toast.success(message: string, duration?: number)
  • toast.error(message: string, duration?: number)
  • toast.info(message: string, duration?: number)
  • toast.warning(message: string, duration?: number)
  • toast.dismiss(id: string)
  • toast.clear()

Features:

  • Auto-dismiss after duration
  • Manual dismiss
  • Stack multiple toasts
  • Position customization
  • Animation support

Data Display Components

RawDataTable

Table component for displaying raw data with minimal styling.

<script lang="ts">
  import RawDataTable from '$lib/components/RawDataTable.svelte';
  
  let data = $state([
    { key: 'Name', value: 'John Doe' },
    { key: 'Email', value: 'john@example.com' },
    { key: 'Role', value: 'Admin' }
  ]);
</script>

<RawDataTable {data} />

Props:

  • data: Record<string, any>[] - Data to display
  • headers: string[] - Column headers (optional)
  • striped: boolean - Alternating row colors

YearFilter

Filter component for selecting academic years.

<script lang="ts">
  import YearFilter from '$lib/components/YearFilter.svelte';
  
  let selectedYear = $state(2024);
  let availableYears = $state([2022, 2023, 2024, 2025]);
</script>

<YearFilter
  bind:selectedYear
  {availableYears}
  onYearChange={(year) => console.log('Year changed:', year)}
/>

Props:

  • selectedYear: number - Currently selected year (use bind:selectedYear)
  • availableYears: number[] - Available years
  • onYearChange: (year: number) => void - Year change callback

Internationalization Components

T

Translation component for i18n.

<script lang="ts">
  import T from '$lib/components/T.svelte';
</script>

<!-- Basic usage -->
<T key="welcome.message" />

<!-- With variables -->
<T key="user.greeting" name="John" />

<!-- With default -->
<T key="optional.message" default="Default text" />

Props:

  • key: string - Translation key
  • default: string - Default text if key not found
  • Additional props passed as translation variables

Theme Components

ThemeBuilder

Component for building and customizing themes.

<script lang="ts">
  import ThemeBuilder from '$lib/components/ThemeBuilder.svelte';
  
  let theme = $state({
    name: 'Custom Theme',
    colors: {
      primary: '#6366f1',
      secondary: '#8b5cf6'
    }
  });
</script>

<ThemeBuilder bind:theme} />

Props:

  • theme: Theme - Theme object (use bind:theme)
  • onSave: (theme: Theme) => void - Save callback

Features:

  • Color picker
  • Preview
  • Export/import
  • Reset to default

Utility Patterns

Loading State Pattern

<script lang="ts">
  import LoadingSpinner from '$lib/components/LoadingSpinner.svelte';
  import EmptyState from '$lib/components/EmptyState.svelte';
  
  let data = $state<any[]>([]);
  let loading = $state(true);
  let error = $state<Error | null>(null);
</script>

{#if loading}
  <LoadingSpinner message="Loading data..." />
{:else if error}
  <div class="p-4 border border-red-500 rounded-lg">
    <p class="text-red-600">{error.message}</p>
    <Button onclick={() => retry()}>Retry</Button>
  </div>
{:else if data.length === 0}
  <EmptyState
    title="No data"
    message="There's nothing here yet"
  />
{:else}
  <!-- Display data -->
  {#each data as item}
    <div>{item.name}</div>
  {/each}
{/if}

Error Handling Pattern

<script lang="ts">
  import ErrorBoundary from '$lib/components/ErrorBoundary.svelte';
  import { toast } from '$lib/stores/toast';
  
  let error = $state<Error | null>(null);
  
  async function loadData() {
    try {
      error = null;
      // Load data
    } catch (e) {
      error = e instanceof Error ? e : new Error('Unknown error');
      toast.error('Failed to load data');
    }
  }
</script>

<ErrorBoundary {error}>
  {#snippet children()}
    <!-- Content -->
  {/snippet}
  
  {#snippet fallback(error)}
    <div class="p-4">
      <h3>Error</h3>
      <p>{error.message}</p>
      <Button onclick={loadData}>Retry</Button>
    </div>
  {/snippet}
</ErrorBoundary>

File Upload Pattern

<script lang="ts">
  import FileUploadButton from '$lib/components/FileUploadButton.svelte';
  import FileCard from '$lib/components/FileCard.svelte';
  import { toast } from '$lib/stores/toast';
  
  let files = $state<FileInfo[]>([]);
  
  async function handleUpload(uploadedFiles: File[]) {
    try {
      // Upload files
      const uploaded = await uploadFiles(uploadedFiles);
      files = [...files, ...uploaded];
      toast.success(`Uploaded ${uploadedFiles.length} file(s)`);
    } catch (error) {
      toast.error('Failed to upload files');
    }
  }
</script>

<div class="space-y-4">
  <FileUploadButton
    accept=".pdf,.doc,.docx"
    multiple={true}
    onUpload={handleUpload}
  />
  
  <div class="grid grid-cols-2 gap-4">
    {#each files as file}
      <FileCard {file} />
    {/each}
  </div>
</div>

Best Practices

  1. Loading States: Always show loading indicators for async operations
  2. Error Handling: Provide clear error messages and retry options
  3. Empty States: Use EmptyState to guide users when there's no data
  4. Toast Notifications: Use toasts for non-critical feedback
  5. File Handling: Validate file types and sizes before upload
  6. Accessibility: Include proper ARIA labels for screen readers
  7. Performance: Lazy load heavy utility components

Component Combinations

Complete Data Loading Pattern

<script lang="ts">
  import LoadingSpinner from '$lib/components/LoadingSpinner.svelte';
  import EmptyState from '$lib/components/EmptyState.svelte';
  import ErrorBoundary from '$lib/components/ErrorBoundary.svelte';
  import { toast } from '$lib/stores/toast';
  
  let data = $state<any[]>([]);
  let loading = $state(true);
  let error = $state<Error | null>(null);
  
  async function loadData() {
    loading = true;
    error = null;
    try {
      data = await fetchData();
    } catch (e) {
      error = e instanceof Error ? e : new Error('Failed to load');
      toast.error('Failed to load data');
    } finally {
      loading = false;
    }
  }
  
  onMount(loadData);
</script>

<ErrorBoundary {error}>
  {#snippet children()}
    {#if loading}
      <LoadingSpinner message="Loading..." />
    {:else if data.length === 0}
      <EmptyState
        title="No data available"
        message="There's nothing to display"
      />
    {:else}
      <!-- Display data -->
    {/if}
  {/snippet}
</ErrorBoundary>

Next Steps