Web (SvelteKit)
Web Architecture
All Manacore web applications use SvelteKit 2 with Svelte 5 (runes mode) and Tailwind CSS.
Project Structure
apps/{project}/apps/web/├── src/│ ├── app.html # HTML template│ ├── app.css # Global styles (Tailwind)│ ├── lib/│ │ ├── components/ # Reusable components│ │ │ ├── ui/ # Base UI components│ │ │ └── features/ # Feature-specific components│ │ ├── stores/ # Svelte stores│ │ ├── services/ # API clients, auth│ │ ├── utils/ # Helper functions│ │ └── types/ # TypeScript types│ └── routes/│ ├── +layout.svelte # Root layout│ ├── +page.svelte # Home page│ ├── (auth)/ # Auth route group│ │ ├── login/│ │ └── register/│ └── (app)/ # App route group (protected)│ ├── +layout.svelte│ ├── dashboard/│ └── settings/├── static/ # Static assets├── svelte.config.js├── vite.config.ts└── tailwind.config.jsSvelte 5 Runes
State Management
<script lang="ts"> // Reactive state let count = $state(0);
// Derived values let doubled = $derived(count * 2);
// Deep reactive objects let user = $state({ name: 'John', settings: { theme: 'dark' } });
// Effects (side effects on state change) $effect(() => { console.log('Count changed:', count); // Cleanup function (optional) return () => console.log('Cleanup'); });
function increment() { count++; }</script>
<button onclick={increment}> Count: {count} (doubled: {doubled})</button>Props
<script lang="ts"> interface Props { title: string; count?: number; onUpdate?: (value: number) => void; }
let { title, count = 0, onUpdate }: Props = $props();</script>
<h1>{title}</h1><p>Count: {count}</p><button onclick={() => onUpdate?.(count + 1)}>Update</button>Bindings
<script lang="ts"> let value = $state(''); let checked = $state(false);</script>
<input bind:value /><input type="checkbox" bind:checked />Routing
Route Groups
Use route groups for layout organization:
routes/├── (marketing)/ # Public pages│ ├── +layout.svelte # Marketing layout│ ├── +page.svelte # Landing page│ └── pricing/├── (auth)/ # Auth pages (no sidebar)│ ├── +layout.svelte│ ├── login/│ └── register/└── (app)/ # Protected app pages ├── +layout.svelte # App layout with sidebar ├── +layout.server.ts # Auth check └── dashboard/Layout Data
// routes/(app)/+layout.server.tsimport { redirect } from '@sveltejs/kit';import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = async ({ locals }) => { if (!locals.user) { throw redirect(302, '/login'); }
return { user: locals.user, };};Page Data
// routes/(app)/dashboard/+page.server.tsimport type { PageServerLoad } from './$types';import { api } from '$lib/services/api';
export const load: PageServerLoad = async ({ locals }) => { const stats = await api.getStats(locals.user.id);
return { stats, };};<!-- routes/(app)/dashboard/+page.svelte --><script lang="ts"> import type { PageData } from './$types';
let { data }: { data: PageData } = $props();</script>
<h1>Dashboard</h1><p>Total items: {data.stats.total}</p>API Integration
API Client
import { PUBLIC_API_URL } from '$env/static/public';
class ApiClient { private baseUrl = PUBLIC_API_URL; private token: string | null = null;
setToken(token: string) { this.token = token; }
private async fetch<T>(path: string, options: RequestInit = {}): Promise<T> { const response = await fetch(`${this.baseUrl}${path}`, { ...options, headers: { 'Content-Type': 'application/json', ...(this.token && { Authorization: `Bearer ${this.token}` }), ...options.headers, }, });
if (!response.ok) { throw new Error(`API Error: ${response.status}`); }
return response.json(); }
async getUsers() { return this.fetch<User[]>('/api/v1/users'); }
async createUser(data: CreateUserDto) { return this.fetch<User>('/api/v1/users', { method: 'POST', body: JSON.stringify(data), }); }}
export const api = new ApiClient();Stores
Use Svelte stores for global state:
import { writable, derived } from 'svelte/store';
interface AuthState { user: User | null; token: string | null; loading: boolean;}
function createAuthStore() { const { subscribe, set, update } = writable<AuthState>({ user: null, token: null, loading: true, });
return { subscribe, setUser: (user: User, token: string) => { update((state) => ({ ...state, user, token, loading: false })); }, logout: () => { set({ user: null, token: null, loading: false }); }, setLoading: (loading: boolean) => { update((state) => ({ ...state, loading })); }, };}
export const auth = createAuthStore();export const isAuthenticated = derived(auth, ($auth) => !!$auth.user);Components
Base Component Pattern
<script lang="ts"> import { cn } from '$lib/utils';
interface Props { variant?: 'primary' | 'secondary' | 'ghost'; size?: 'sm' | 'md' | 'lg'; disabled?: boolean; class?: string; onclick?: () => void; }
let { variant = 'primary', size = 'md', disabled = false, class: className, onclick, ...rest }: Props = $props();
const variants = { primary: 'bg-blue-500 text-white hover:bg-blue-600', secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300', ghost: 'bg-transparent hover:bg-gray-100', };
const sizes = { sm: 'px-2 py-1 text-sm', md: 'px-4 py-2', lg: 'px-6 py-3 text-lg', };</script>
<button class={cn( 'rounded-md font-medium transition-colors', variants[variant], sizes[size], disabled && 'opacity-50 cursor-not-allowed', className )} {disabled} {onclick} {...rest}> <slot /></button>Utility Function
import { clsx, type ClassValue } from 'clsx';import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs));}Forms
Form Actions
import type { Actions } from './$types';import { fail } from '@sveltejs/kit';
export const actions: Actions = { updateProfile: async ({ request, locals }) => { const data = await request.formData(); const name = data.get('name') as string;
if (!name || name.length < 2) { return fail(400, { error: 'Name must be at least 2 characters' }); }
await api.updateUser(locals.user.id, { name });
return { success: true }; },};<script lang="ts"> import { enhance } from '$app/forms'; import type { ActionData } from './$types';
let { form }: { form: ActionData } = $props();</script>
<form method="POST" action="?/updateProfile" use:enhance> <input name="name" required minlength="2" />
{#if form?.error} <p class="text-red-500">{form.error}</p> {/if}
<button type="submit">Save</button></form>Best Practices
- Use runes - Always use
$state,$derived,$effect - Type everything - Use TypeScript for all components
- Server-side data loading - Use
+page.server.tsfor API calls - Form actions - Use SvelteKit form actions for mutations
- Tailwind - Use utility classes, avoid custom CSS
- Component composition - Build small, reusable components