DesQTA follows a component-based architecture with clear separation between UI components, business logic, and data. This guide covers component patterns, composition, and best practices.
src/lib/components/
├── ui/ # Base UI components (buttons, cards, inputs)
├── layout/ # Layout components (header, sidebar)
├── content/ # Content components (assessments, timetable)
├── [feature]/ # Feature-specific components
└── [shared]/ # Shared utility components
Pure UI components that receive props and emit events:
<!-- Button.svelte -->
< script lang = "ts" >
interface Props {
label : string ;
variant ?: 'primary' | 'secondary' ;
onclick ?: () => void ;
}
let { label, variant = 'primary' , onclick } : Props = $ props ();
</ script >
< button
class = "btn btn-{ variant }"
onclick ={onclick}
>
{label}
</ button >
Components that manage state and business logic:
<!-- AssessmentList.svelte -->
< script lang = "ts" >
import { onMount } from 'svelte' ;
import { assessmentService } from '$lib/services/assessmentService' ;
import AssessmentCard from './AssessmentCard.svelte' ;
let assessments = $ state < Assessment []>([]);
let loading = $ state ( true );
onMount ( async () => {
assessments = await assessmentService. loadAssessments () || [];
loading = false ;
});
</ script >
{# if loading}
< LoadingSpinner />
{: else }
{# each assessments as assessment (assessment.id)}
< AssessmentCard { assessment } />
{/ each }
{/ if }
Components that structure the page layout:
<!-- AppLayout.svelte -->
< script lang = "ts" >
let { children } : { children : import ( 'svelte' ). Snippet } = $ props ();
</ script >
< div class = "app-layout" >
< AppHeader />
< div class = "content" >
< AppSidebar />
< main >
{@ render children ()}
</ main >
</ div >
</ div >
<!-- Input.svelte -->
< script lang = "ts" >
interface Props {
value : string ;
onchange ?: ( value : string ) => void ;
placeholder ?: string ;
}
let { value, onchange, placeholder } : Props = $ props ();
function handleInput ( event : Event ) {
const target = event.target as HTMLInputElement ;
onchange ?.(target.value);
}
</ script >
< input
type = "text"
{ value }
{ placeholder }
oninput ={handleInput}
/>
<!-- Input.svelte -->
< script lang = "ts" >
interface Props {
defaultValue ?: string ;
placeholder ?: string ;
}
let { defaultValue = '' , placeholder } : Props = $ props ();
let input : HTMLInputElement ;
</ script >
< input
bind : this ={input}
type = "text"
value ={defaultValue}
{ placeholder }
/>
<!-- Access value via input.value -->
<!-- Card.svelte -->
< script lang = "ts" >
let { children, header, footer } : {
children : import ( 'svelte' ). Snippet ;
header ?: import ( 'svelte' ). Snippet ;
footer ?: import ( 'svelte' ). Snippet ;
} = $ props ();
</ script >
< div class = "card" >
{# if header}
< header >
{@ render header ()}
</ header >
{/ if }
< main >
{@ render children ()}
</ main >
{# if footer}
< footer >
{@ render footer ()}
</ footer >
{/ if }
</ div >
<!-- Usage -->
< Card >
< svelte : fragment slot = "header" >
< h2 >Title</ h2 >
</ svelte : fragment >
< p >Content</ p >
< svelte : fragment slot = "footer" >
< button >Action</ button >
</ svelte : fragment >
</ Card >
<!-- DataLoader.svelte -->
< script lang = "ts" >
interface Props {
loader : () => Promise < any >;
children : ( data : any , loading : boolean , error : string | null ) => import ( 'svelte' ). Snippet ;
}
let { loader, children } : Props = $ props ();
let data = $ state < any >( null );
let loading = $ state ( true );
let error = $ state < string | null >( null );
onMount ( async () => {
try {
data = await loader ();
} catch (e) {
error = e instanceof Error ? e.message : 'Unknown error' ;
} finally {
loading = false ;
}
});
</ script >
{@ render children (data, loading, error)}
<!-- AssessmentCard.svelte -->
< script lang = "ts" >
import { Badge } from '$lib/components/ui' ;
import { formatDate } from '$lib/utils/date' ;
interface Props {
assessment : Assessment ;
onclick ?: () => void ;
}
let { assessment, onclick } : Props = $ props ();
let status = $ derived (
assessment.dueDate < new Date () ? 'overdue' :
assessment.dueDate < new Date (Date. now () + 7 * 24 * 60 * 60 * 1000 ) ? 'due-soon' :
'upcoming'
);
</ script >
< div class = "assessment-card" onclick ={onclick}>
< div class = "header" >
< h3 >{assessment.title}</ h3 >
< Badge variant ={status}>{status}</ Badge >
</ div >
< div class = "body" >
< p >{assessment.subject}</ p >
< p >Due: { formatDate (assessment.dueDate)}</ p >
</ div >
</ div >
DesQTA uses a custom UI component library based on shadcn-vue patterns:
<!-- src/lib/components/ui/Button.svelte -->
< script lang = "ts" >
interface Props {
variant ?: 'primary' | 'secondary' | 'outline' | 'ghost' ;
size ?: 'sm' | 'md' | 'lg' ;
disabled ?: boolean ;
onclick ?: () => void ;
}
let {
variant = 'primary' ,
size = 'md' ,
disabled = false ,
onclick
} : Props = $ props ();
</ script >
< button
class = "btn btn-{ variant } btn-{ size }"
{ disabled }
onclick ={onclick}
>
< slot />
</ button >
<!-- Card.svelte -->
< script lang = "ts" >
interface Props {
variant ?: 'default' | 'outlined' | 'elevated' ;
padding ?: 'none' | 'sm' | 'md' | 'lg' ;
}
let { variant = 'default' , padding = 'md' } : Props = $ props ();
</ script >
< div class = "card card-{ variant } card-padding-{ padding }" >
< slot />
</ div >
< script lang = "ts" >
let isOpen = $ state ( false );
let selectedItem = $ state < Item | null >( null );
function toggle () {
isOpen = ! isOpen;
}
</ script >
< script lang = "ts" >
interface Props {
initialValue : string ;
}
let { initialValue } : Props = $ props ();
let value = $ state (initialValue);
// Sync with prop changes
$ effect (() => {
value = initialValue;
});
</ script >
< script lang = "ts" >
let items = $ state < Item []>([]);
let filter = $ state ( '' );
let filteredItems = $ derived (
items. filter ( item => item.name. includes (filter))
);
let itemCount = $ derived (filteredItems. length );
</ script >
< script lang = "ts" >
import { onMount } from 'svelte' ;
let data = $ state < Data | null >( null );
onMount ( async () => {
data = await fetchData ();
// Cleanup function
return () => {
cleanup ();
};
});
</ script >
< script lang = "ts" >
import { onDestroy } from 'svelte' ;
let interval : ReturnType < typeof setInterval>;
onMount (() => {
interval = setInterval (() => {
update ();
}, 1000 );
});
onDestroy (() => {
clearInterval (interval);
});
</ script >
< script lang = "ts" >
import { beforeUpdate, afterUpdate } from 'svelte' ;
let scrollHeight = $ state ( 0 );
beforeUpdate (() => {
scrollHeight = document.documentElement.scrollHeight;
});
afterUpdate (() => {
if (document.documentElement.scrollHeight !== scrollHeight) {
// Height changed
}
});
</ script >
<!-- Parent -->
< ChildComponent data ={data} onchange ={handleChange} />
<!-- Child -->
< script lang = "ts" >
interface Props {
data : Data ;
onchange ?: ( value : string ) => void ;
}
let { data, onchange } : Props = $ props ();
</ script >
<!-- Child -->
< script lang = "ts" >
import { createEventDispatcher } from 'svelte' ;
const dispatch = createEventDispatcher ();
function handleClick () {
dispatch ( 'click' , { data: 'value' });
}
</ script >
< button onclick ={handleClick}>Click</ button >
<!-- Parent -->
< ChildComponent on : click ={( e ) => console. log (e.detail)} />
<!-- Provider -->
< script lang = "ts" >
import { setContext } from 'svelte' ;
const themeContext = {
theme: 'dark' ,
toggleTheme : () => { /* ... */ }
};
setContext ( 'theme' , themeContext);
</ script >
<!-- Consumer -->
< script lang = "ts" >
import { getContext } from 'svelte' ;
const theme = getContext ( 'theme' );
</ script >
// AssessmentCard.test.ts
import { render } from '@testing-library/svelte' ;
import AssessmentCard from './AssessmentCard.svelte' ;
test ( 'renders assessment title' , () => {
const { getByText } = render (AssessmentCard, {
assessment: {
id: 1 ,
title: 'Math Test' ,
dueDate: new Date ()
}
});
expect ( getByText ( 'Math Test' )). toBeInTheDocument ();
});
<!-- ✅ Good - Focused component -->
< AssessmentCard { assessment } />
<!-- ❌ Avoid - Too many responsibilities -->
< AssessmentCardWithFiltersAndSortingAndExport />
<!-- ✅ Good - Composable -->
< Card >
< CardHeader >Title</ CardHeader >
< CardContent >Content</ CardContent >
</ Card >
<!-- ❌ Avoid - Too many props -->
< Card header = "Title" content = "Content" footer = "Footer" />
<!-- ✅ Good - Logic in service -->
< script lang = "ts" >
import { assessmentService } from '$lib/services/assessmentService' ;
const data = await assessmentService. load ();
</ script >
<!-- ❌ Avoid - Logic in component -->
< script lang = "ts" >
const data = await fetch ( '/api/assessments' ). then ( r => r. json ());
</ script >