Cleanup: Remove old modes, add Svelte 5 reference library, update orchestrator

- Delete Mode1Test, Mode2Test, Mode3Test, Mode5Test (keep only Mode6Test)
- Delete old documentation, Docker files, and k8s configs
- Add comprehensive Svelte 5 reference library (svelte-reference/)
- Update ModeSwitch orchestrator:
  - Fix port to 3005 (never use 3000/3001)
  - Add API endpoints for mode switching
  - Add bun-types and proper tsconfig
  - Clean up mode folder mappings
This commit is contained in:
2026-01-21 03:56:01 +00:00
parent 42ff3e8f33
commit 0b76a4b119
223 changed files with 8861 additions and 362834 deletions
+343
View File
@@ -0,0 +1,343 @@
# Svelte 5 Quick Reference Card
## Runes Cheat Sheet
```svelte
<script>
// ═══════════════════════════════════════════════════════════
// $state - Reactive state declaration
// ═══════════════════════════════════════════════════════════
let count = $state(0); // Primitive
let user = $state({ name: '', age: 0 }); // Object (deep reactive)
let items = $state<string[]>([]); // Array (deep reactive)
// Direct mutations work with $state!
items.push('item'); // ✅ Reactive
user.name = 'John'; // ✅ Reactive
count++; // ✅ Reactive
// ═══════════════════════════════════════════════════════════
// $derived - Computed values (auto-tracks dependencies)
// ═══════════════════════════════════════════════════════════
let doubled = $derived(count * 2); // Simple expression
let filtered = $derived.by(() => { // Complex computation
return items.filter(i => i.includes(filter));
});
// ═══════════════════════════════════════════════════════════
// $effect - Side effects (auto-tracks, auto-cleanup)
// ═══════════════════════════════════════════════════════════
$effect(() => {
console.log('Count is:', count); // Runs when count changes
return () => { // Optional cleanup
console.log('Cleaning up');
};
});
$effect.pre(() => {
// Runs BEFORE DOM updates
});
// ═══════════════════════════════════════════════════════════
// $props - Component props
// ═══════════════════════════════════════════════════════════
let {
name, // Required prop
count = 0, // With default
children, // Snippet (replaces default slot)
header, // Named snippet
onclick, // Event callback
...rest // Rest props
} = $props();
// ═══════════════════════════════════════════════════════════
// $bindable - Two-way bindable props
// ═══════════════════════════════════════════════════════════
let { value = $bindable('') } = $props();
// Parent: <Input bind:value />
// ═══════════════════════════════════════════════════════════
// $inspect - Debug reactive values (dev only)
// ═══════════════════════════════════════════════════════════
$inspect(count); // Logs on change
$inspect(count).with(console.trace); // Custom logger
</script>
```
---
## Template Syntax
```svelte
<!-- Rendering Snippets (replaces slots) -->
{@render children?.()}
{@render header?.()}
{@render row({ item, index })}
<!-- Conditionals -->
{#if condition}
...
{:else if other}
...
{:else}
...
{/if}
<!-- Loops -->
{#each items as item, index (item.id)}
...
{/each}
<!-- Await -->
{#await promise}
<Loading />
{:then data}
<Result {data} />
{:catch error}
<Error {error} />
{/await}
<!-- Key block (force re-render) -->
{#key value}
<Component />
{/key}
<!-- HTML (dangerous) -->
{@html content}
<!-- Debug -->
{@debug variable}
<!-- Define snippets -->
{#snippet name(param1, param2)}
<div>{param1}</div>
{/snippet}
```
---
## Component Patterns
```svelte
<!-- Basic Component -->
<script lang="ts">
import type { Snippet } from 'svelte';
interface Props {
title: string;
variant?: 'primary' | 'secondary';
disabled?: boolean;
onclick?: () => void;
children: Snippet;
}
let {
title,
variant = 'primary',
disabled = false,
onclick,
children
}: Props = $props();
</script>
<button
class="btn btn-{variant}"
{disabled}
{onclick}
>
{@render children()}
</button>
<!-- With Named Snippets -->
<script lang="ts">
import type { Snippet } from 'svelte';
interface Props {
header?: Snippet;
children?: Snippet;
footer?: Snippet<[{ close: () => void }]>;
}
let { header, children, footer }: Props = $props();
function close() {
// close logic
}
</script>
{@render header?.()}
{@render children?.()}
{@render footer?.({ close })}
```
---
## State Management
```typescript
// stores/counter.svelte.ts
export class CounterStore {
count = $state(0);
doubled = $derived(this.count * 2);
increment = () => { this.count++; }
decrement = () => { this.count--; }
reset = () => { this.count = 0; }
}
export const counter = new CounterStore();
// Usage in component
import { counter } from '$lib/stores/counter.svelte';
// Read: counter.count, counter.doubled
// Write: counter.increment()
```
---
## SvelteKit Essentials
```typescript
// +page.ts (Universal load)
export async function load({ params, fetch }) {
const data = await fetch(`/api/items/${params.id}`);
return { item: await data.json() };
}
// +page.server.ts (Server-only load)
export async function load({ params, locals }) {
return { user: locals.user };
}
// Form Actions
export const actions = {
default: async ({ request }) => {
const data = await request.formData();
// Process form
return { success: true };
},
named: async ({ request }) => {
// Named action: <form action="?/named">
}
};
// API Routes (+server.ts)
export async function GET({ params }) {
return json({ data: 'value' });
}
export async function POST({ request }) {
const body = await request.json();
return json({ created: true }, { status: 201 });
}
```
---
## File Structure
```
src/
├── app.html # HTML template
├── app.d.ts # Type declarations
├── lib/
│ ├── components/ # Reusable components
│ ├── stores/ # State (.svelte.ts files)
│ ├── utils/ # Utility functions
│ ├── hooks/ # Custom hooks
│ └── server/ # Server-only code
├── routes/
│ ├── +page.svelte # Page component
│ ├── +page.ts # Page load (universal)
│ ├── +page.server.ts # Page load (server) + actions
│ ├── +layout.svelte # Layout component
│ ├── +layout.ts # Layout load
│ ├── +error.svelte # Error page
│ └── api/
│ └── +server.ts # API endpoint
└── hooks.server.ts # Server hooks
```
---
## Common Anti-Patterns
```svelte
<!-- ❌ DON'T: Destructure reactive state -->
let { name } = user; // Loses reactivity!
<!-- ✅ DO: Access properties directly -->
{user.name}
<!-- ❌ DON'T: Use $effect for derived values -->
$effect(() => {
doubled = count * 2; // Use $derived instead!
});
<!-- ✅ DO: Use $derived -->
let doubled = $derived(count * 2);
<!-- ❌ DON'T: Forget optional chaining for snippets -->
{@render children()} // May error if undefined
<!-- ✅ DO: Use optional chaining -->
{@render children?.()}
<!-- ❌ DON'T: Modify state in $derived -->
let bad = $derived(count++); // Side effect!
<!-- ✅ DO: Keep $derived pure -->
let good = $derived(count + 1);
```
---
## TypeScript Quick Types
```typescript
import type {
Snippet, // Snippet type
Component, // Component type
ComponentProps // Extract props from component
} from 'svelte';
import type {
PageData, // From +page.ts load
PageServerData, // From +page.server.ts load
Actions, // Form actions type
RequestHandler // API route handler
} from './$types';
// Snippet with params
type ItemSnippet = Snippet<[{ item: Item; index: number }]>;
// Component props
type ButtonProps = ComponentProps<typeof Button>;
```
---
## Key Commands
```bash
# Create new project
npx sv create my-app
# Development
npm run dev
# Build
npm run build
# Preview production build
npm run preview
# Type checking
npm run check
# Run migration
npx sv migrate svelte-5
```
+439
View File
@@ -0,0 +1,439 @@
# Svelte 5 Runes Complete Guide
## Overview
Runes are Svelte 5's new reactivity system. They replace the old `$:` reactive statements and `export let` declarations with explicit, predictable primitives.
---
## 📦 `$state` - Reactive State
### Basic Usage
```svelte
<script lang="ts">
let count = $state(0);
let user = $state({ name: 'John', age: 30 });
let items = $state<string[]>([]);
</script>
<button onclick={() => count++}>
Clicked {count} times
</button>
```
### Deep Reactivity
```svelte
<script lang="ts">
// Objects and arrays are deeply reactive
let todos = $state([
{ id: 1, text: 'Learn Svelte', done: false }
]);
function toggle(id: number) {
const todo = todos.find(t => t.id === id);
if (todo) todo.done = !todo.done; // This triggers updates!
}
</script>
```
### `$state.raw` - Shallow Reactivity
```svelte
<script lang="ts">
// Use for large objects where you replace, not mutate
let data = $state.raw<BigObject | null>(null);
async function load() {
data = await fetchData(); // Must reassign, not mutate
}
</script>
```
### `$state.snapshot` - Get Plain Object
```svelte
<script lang="ts">
let user = $state({ name: 'John' });
function save() {
// Get a plain, non-reactive copy
const plain = $state.snapshot(user);
localStorage.setItem('user', JSON.stringify(plain));
}
</script>
```
---
## 🔗 `$derived` - Computed Values
### Basic Derived
```svelte
<script lang="ts">
let count = $state(0);
let doubled = $derived(count * 2);
let quadrupled = $derived(doubled * 2);
</script>
<p>{count} × 2 = {doubled}</p>
<p>{doubled} × 2 = {quadrupled}</p>
```
### Complex Derived with `$derived.by`
```svelte
<script lang="ts">
let items = $state([1, 2, 3, 4, 5]);
let filter = $state('all');
// Use $derived.by for complex computations
let filtered = $derived.by(() => {
console.log('Recomputing filtered list...');
if (filter === 'even') {
return items.filter(n => n % 2 === 0);
}
if (filter === 'odd') {
return items.filter(n => n % 2 !== 0);
}
return items;
});
</script>
```
### Derived from Multiple Sources
```svelte
<script lang="ts">
let firstName = $state('John');
let lastName = $state('Doe');
let fullName = $derived(`${firstName} ${lastName}`);
let initials = $derived(`${firstName[0]}${lastName[0]}`);
</script>
```
---
## ⚡ `$effect` - Side Effects
### Basic Effect
```svelte
<script lang="ts">
let count = $state(0);
$effect(() => {
// Runs when count changes
console.log(`Count is now ${count}`);
document.title = `Count: ${count}`;
});
</script>
```
### Effect with Cleanup
```svelte
<script lang="ts">
let running = $state(false);
$effect(() => {
if (!running) return;
const interval = setInterval(() => {
console.log('tick');
}, 1000);
// Cleanup function
return () => {
clearInterval(interval);
};
});
</script>
```
### `$effect.pre` - Before DOM Update
```svelte
<script lang="ts">
let messages = $state<string[]>([]);
let container: HTMLDivElement;
$effect.pre(() => {
// Runs BEFORE DOM updates
// Useful for preserving scroll position
messages; // Track dependency
if (container) {
// Save scroll position before new messages render
}
});
</script>
```
### `$effect.tracking` - Check if Reactive
```svelte
<script lang="ts">
$effect(() => {
console.log($effect.tracking()); // true
});
function regularFunction() {
console.log($effect.tracking()); // false
}
</script>
```
### `$effect.root` - Untracked Effect Scope
```svelte
<script lang="ts">
// Create effects outside component lifecycle
const cleanup = $effect.root(() => {
$effect(() => {
// This effect lives until cleanup() is called
});
return () => {
// Custom cleanup logic
};
});
// Later: cleanup();
</script>
```
---
## 🎯 `$props` - Component Props
### Basic Props
```svelte
<script lang="ts">
let { name, age }: { name: string; age: number } = $props();
</script>
<p>{name} is {age} years old</p>
```
### Optional Props with Defaults
```svelte
<script lang="ts">
interface Props {
name: string;
age?: number;
greeting?: string;
}
let {
name,
age = 18,
greeting = 'Hello'
}: Props = $props();
</script>
<p>{greeting}, {name}! You are {age}.</p>
```
### Rest Props
```svelte
<script lang="ts">
import type { HTMLButtonAttributes } from 'svelte/elements';
interface Props extends HTMLButtonAttributes {
variant?: 'primary' | 'secondary';
}
let { variant = 'primary', ...rest }: Props = $props();
</script>
<button class={variant} {...rest}>
<slot />
</button>
```
### Children as Props (Snippets)
```svelte
<script lang="ts">
import type { Snippet } from 'svelte';
interface Props {
header?: Snippet;
children: Snippet;
footer?: Snippet;
}
let { header, children, footer }: Props = $props();
</script>
<div class="card">
{#if header}
<header>{@render header()}</header>
{/if}
<main>{@render children()}</main>
{#if footer}
<footer>{@render footer()}</footer>
{/if}
</div>
```
---
## 🔄 `$bindable` - Two-Way Binding Props
### Basic Bindable
```svelte
<!-- TextInput.svelte -->
<script lang="ts">
let { value = $bindable('') }: { value?: string } = $props();
</script>
<input bind:value />
<!-- Parent.svelte -->
<script lang="ts">
import TextInput from './TextInput.svelte';
let name = $state('');
</script>
<TextInput bind:value={name} />
<p>Name: {name}</p>
```
### Bindable with Fallback
```svelte
<script lang="ts">
// Provide a fallback for when parent doesn't bind
let { checked = $bindable(false) }: { checked?: boolean } = $props();
</script>
<input type="checkbox" bind:checked />
```
---
## 🎨 `$inspect` - Development Debugging
### Basic Inspect
```svelte
<script lang="ts">
let count = $state(0);
let user = $state({ name: 'John' });
// Logs whenever count or user changes
$inspect(count, user);
</script>
```
### Custom Inspect Handler
```svelte
<script lang="ts">
let data = $state({ x: 0, y: 0 });
$inspect(data).with((type, ...values) => {
if (type === 'update') {
console.log('Data updated:', values);
}
});
</script>
```
### Breakpoint on Change
```svelte
<script lang="ts">
let buggyValue = $state(0);
// Pauses debugger when value changes
$inspect(buggyValue).with(() => {
debugger;
});
</script>
```
---
## 🏪 Class-Based State with `$state`
```svelte
<script lang="ts" module>
// Can be in a separate .svelte.ts file
export class Counter {
count = $state(0);
doubled = $derived(this.count * 2);
increment() {
this.count++;
}
decrement() {
this.count--;
}
reset() {
this.count = 0;
}
}
</script>
<script lang="ts">
const counter = new Counter();
</script>
<button onclick={() => counter.decrement()}>-</button>
<span>{counter.count} (doubled: {counter.doubled})</span>
<button onclick={() => counter.increment()}>+</button>
```
---
## ⚠️ Common Mistakes
### ❌ Wrong: Destructuring Loses Reactivity
```svelte
<script lang="ts">
let user = $state({ name: 'John', age: 30 });
// ❌ This breaks reactivity!
let { name, age } = user;
// ✅ Access properties directly
// user.name, user.age
</script>
```
### ❌ Wrong: Effect Without Dependencies
```svelte
<script lang="ts">
let count = $state(0);
// ❌ This runs only once (no reactive reads inside)
$effect(() => {
console.log('This only runs once');
});
// ✅ Read reactive values to create dependencies
$effect(() => {
console.log(`Count is ${count}`);
});
</script>
```
### ❌ Wrong: Mutating Derived Values
```svelte
<script lang="ts">
let items = $state([1, 2, 3]);
let doubled = $derived(items.map(n => n * 2));
// ❌ Cannot mutate derived values!
// doubled.push(8);
// ✅ Mutate the source
items.push(4);
</script>
```
---
## 📊 Runes Comparison Table
| Svelte 4 | Svelte 5 | Purpose |
|----------|----------|---------|
| `export let x` | `let { x } = $props()` | Props |
| `let x = 0` | `let x = $state(0)` | Reactive state |
| `$: y = x * 2` | `let y = $derived(x * 2)` | Computed |
| `$: { ... }` | `$effect(() => { ... })` | Side effects |
| `$$props` | `$props()` with rest | All props |
| `bind:value` | `$bindable()` | Two-way binding |
+693
View File
@@ -0,0 +1,693 @@
# Svelte 5 Component Patterns
## 📦 Component Structure Template
Every Svelte component should follow this structure:
```svelte
<script lang="ts" module>
// Module-level code (runs once, shared between instances)
// Use for types, constants, or shared state
export interface ButtonProps {
variant?: 'primary' | 'secondary' | 'danger';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
}
</script>
<script lang="ts">
// Instance-level code (runs for each component instance)
import type { Snippet } from 'svelte';
import type { HTMLButtonAttributes } from 'svelte/elements';
interface Props extends ButtonProps, HTMLButtonAttributes {
children: Snippet;
}
let {
variant = 'primary',
size = 'md',
disabled = false,
children,
...rest
}: Props = $props();
// Derived values
let classes = $derived(
`btn btn-${variant} btn-${size} ${disabled ? 'btn-disabled' : ''}`
);
</script>
<button class={classes} {disabled} {...rest}>
{@render children()}
</button>
<style>
.btn {
/* Base styles */
}
</style>
```
---
## 🎨 Pattern 1: Presentational Components
Pure UI components with no business logic.
```svelte
<!-- Card.svelte -->
<script lang="ts">
import type { Snippet } from 'svelte';
interface Props {
title?: string;
header?: Snippet;
children: Snippet;
footer?: Snippet;
variant?: 'default' | 'elevated' | 'outlined';
}
let {
title,
header,
children,
footer,
variant = 'default'
}: Props = $props();
</script>
<article class="card card-{variant}">
{#if header}
<header class="card-header">
{@render header()}
</header>
{:else if title}
<header class="card-header">
<h3>{title}</h3>
</header>
{/if}
<div class="card-body">
{@render children()}
</div>
{#if footer}
<footer class="card-footer">
{@render footer()}
</footer>
{/if}
</article>
<style>
.card {
border-radius: 0.5rem;
overflow: hidden;
}
.card-default {
background: var(--color-surface);
border: 1px solid var(--color-border);
}
.card-elevated {
background: var(--color-surface);
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
}
.card-outlined {
background: transparent;
border: 2px solid var(--color-border);
}
</style>
```
---
## 🔧 Pattern 2: Container Components
Components that manage state and pass it down.
```svelte
<!-- TodoList.svelte -->
<script lang="ts">
import TodoItem from './TodoItem.svelte';
import AddTodo from './AddTodo.svelte';
interface Todo {
id: number;
text: string;
completed: boolean;
}
let todos = $state<Todo[]>([]);
let filter = $state<'all' | 'active' | 'completed'>('all');
let filteredTodos = $derived.by(() => {
switch (filter) {
case 'active': return todos.filter(t => !t.completed);
case 'completed': return todos.filter(t => t.completed);
default: return todos;
}
});
let stats = $derived({
total: todos.length,
active: todos.filter(t => !t.completed).length,
completed: todos.filter(t => t.completed).length
});
function addTodo(text: string) {
todos.push({
id: Date.now(),
text,
completed: false
});
}
function toggleTodo(id: number) {
const todo = todos.find(t => t.id === id);
if (todo) todo.completed = !todo.completed;
}
function deleteTodo(id: number) {
const index = todos.findIndex(t => t.id === id);
if (index !== -1) todos.splice(index, 1);
}
</script>
<div class="todo-list">
<AddTodo onadd={addTodo} />
<div class="filters">
<button
class:active={filter === 'all'}
onclick={() => filter = 'all'}
>
All ({stats.total})
</button>
<button
class:active={filter === 'active'}
onclick={() => filter = 'active'}
>
Active ({stats.active})
</button>
<button
class:active={filter === 'completed'}
onclick={() => filter = 'completed'}
>
Completed ({stats.completed})
</button>
</div>
<ul>
{#each filteredTodos as todo (todo.id)}
<TodoItem
{todo}
ontoggle={() => toggleTodo(todo.id)}
ondelete={() => deleteTodo(todo.id)}
/>
{/each}
</ul>
</div>
```
---
## 🔄 Pattern 3: Controlled Components
Components where parent controls the value.
```svelte
<!-- Select.svelte -->
<script lang="ts">
import type { Snippet } from 'svelte';
interface Option {
value: string;
label: string;
disabled?: boolean;
}
interface Props {
value: string;
options: Option[];
placeholder?: string;
disabled?: boolean;
onchange?: (value: string) => void;
}
let {
value,
options,
placeholder = 'Select an option',
disabled = false,
onchange
}: Props = $props();
let open = $state(false);
let selectedLabel = $derived(
options.find(o => o.value === value)?.label ?? placeholder
);
function select(option: Option) {
if (option.disabled) return;
onchange?.(option.value);
open = false;
}
</script>
<div class="select" class:open class:disabled>
<button
type="button"
class="select-trigger"
onclick={() => open = !open}
{disabled}
>
<span>{selectedLabel}</span>
<svg class="chevron"><!-- icon --></svg>
</button>
{#if open}
<ul class="select-options">
{#each options as option (option.value)}
<li>
<button
type="button"
class:selected={option.value === value}
class:disabled={option.disabled}
onclick={() => select(option)}
>
{option.label}
</button>
</li>
{/each}
</ul>
{/if}
</div>
```
---
## 🎭 Pattern 4: Compound Components
Related components that work together.
```svelte
<!-- Tabs.svelte -->
<script lang="ts" module>
import { setContext, getContext } from 'svelte';
const TABS_KEY = Symbol('tabs');
interface TabsContext {
activeTab: { value: string };
setActiveTab: (id: string) => void;
}
export function getTabsContext(): TabsContext {
return getContext(TABS_KEY);
}
</script>
<script lang="ts">
import type { Snippet } from 'svelte';
interface Props {
defaultTab?: string;
children: Snippet;
}
let { defaultTab = '', children }: Props = $props();
let activeTab = $state(defaultTab);
setContext<TabsContext>(TABS_KEY, {
get activeTab() { return { value: activeTab }; },
setActiveTab: (id: string) => { activeTab = id; }
});
</script>
<div class="tabs">
{@render children()}
</div>
<!-- TabList.svelte -->
<script lang="ts">
import type { Snippet } from 'svelte';
interface Props {
children: Snippet;
}
let { children }: Props = $props();
</script>
<div role="tablist" class="tab-list">
{@render children()}
</div>
<!-- Tab.svelte -->
<script lang="ts">
import { getTabsContext } from './Tabs.svelte';
import type { Snippet } from 'svelte';
interface Props {
id: string;
children: Snippet;
}
let { id, children }: Props = $props();
const { activeTab, setActiveTab } = getTabsContext();
let isActive = $derived(activeTab.value === id);
</script>
<button
role="tab"
aria-selected={isActive}
class:active={isActive}
onclick={() => setActiveTab(id)}
>
{@render children()}
</button>
<!-- TabPanel.svelte -->
<script lang="ts">
import { getTabsContext } from './Tabs.svelte';
import type { Snippet } from 'svelte';
interface Props {
id: string;
children: Snippet;
}
let { id, children }: Props = $props();
const { activeTab } = getTabsContext();
let isActive = $derived(activeTab.value === id);
</script>
{#if isActive}
<div role="tabpanel">
{@render children()}
</div>
{/if}
<!-- Usage -->
<!--
<Tabs defaultTab="tab1">
<TabList>
<Tab id="tab1">First</Tab>
<Tab id="tab2">Second</Tab>
</TabList>
<TabPanel id="tab1">Content 1</TabPanel>
<TabPanel id="tab2">Content 2</TabPanel>
</Tabs>
-->
```
---
## 🔀 Pattern 5: Render Props with Snippets
Pass rendering control to parent.
```svelte
<!-- List.svelte -->
<script lang="ts" generics="T">
import type { Snippet } from 'svelte';
interface Props {
items: T[];
renderItem: Snippet<[T, number]>;
renderEmpty?: Snippet;
}
let { items, renderItem, renderEmpty }: Props = $props();
</script>
{#if items.length === 0}
{#if renderEmpty}
{@render renderEmpty()}
{:else}
<p>No items</p>
{/if}
{:else}
<ul>
{#each items as item, index (index)}
<li>
{@render renderItem(item, index)}
</li>
{/each}
</ul>
{/if}
<!-- Usage -->
<!--
<List items={users}>
{#snippet renderItem(user, index)}
<div class="user">
<span>{index + 1}.</span>
<span>{user.name}</span>
</div>
{/snippet}
{#snippet renderEmpty()}
<p>No users found. Create one?</p>
{/snippet}
</List>
-->
```
---
## 🎛️ Pattern 6: Form Components
Accessible, validated form inputs.
```svelte
<!-- FormField.svelte -->
<script lang="ts">
import type { Snippet } from 'svelte';
interface Props {
name: string;
label: string;
error?: string;
required?: boolean;
children: Snippet<[{ id: string; invalid: boolean }]>;
}
let { name, label, error, required = false, children }: Props = $props();
let id = $derived(`field-${name}`);
let errorId = $derived(`${id}-error`);
let invalid = $derived(!!error);
</script>
<div class="form-field" class:invalid>
<label for={id}>
{label}
{#if required}
<span class="required" aria-hidden="true">*</span>
{/if}
</label>
{@render children({ id, invalid })}
{#if error}
<p id={errorId} class="error" role="alert">
{error}
</p>
{/if}
</div>
<!-- TextInput.svelte -->
<script lang="ts">
import type { HTMLInputAttributes } from 'svelte/elements';
interface Props extends HTMLInputAttributes {
value?: string;
invalid?: boolean;
}
let { value = $bindable(''), invalid = false, ...rest }: Props = $props();
</script>
<input
type="text"
bind:value
aria-invalid={invalid}
{...rest}
/>
<!-- Usage -->
<!--
<FormField name="email" label="Email" error={errors.email} required>
{#snippet children({ id, invalid })}
<TextInput
{id}
{invalid}
type="email"
bind:value={form.email}
placeholder="you@example.com"
/>
{/snippet}
</FormField>
-->
```
---
## 🚦 Pattern 7: Loading States
Handle async data gracefully.
```svelte
<!-- AsyncContent.svelte -->
<script lang="ts" generics="T">
import type { Snippet } from 'svelte';
interface Props {
promise: Promise<T>;
loading?: Snippet;
error?: Snippet<[Error]>;
children: Snippet<[T]>;
}
let { promise, loading, error, children }: Props = $props();
</script>
{#await promise}
{#if loading}
{@render loading()}
{:else}
<div class="loading">Loading...</div>
{/if}
{:then data}
{@render children(data)}
{:catch err}
{#if error}
{@render error(err)}
{:else}
<div class="error">Error: {err.message}</div>
{/if}
{/await}
<!-- Usage -->
<!--
<AsyncContent promise={fetchUsers()}>
{#snippet loading()}
<Skeleton count={5} />
{/snippet}
{#snippet children(users)}
<UserList {users} />
{/snippet}
{#snippet error(err)}
<Alert variant="error">
Failed to load users: {err.message}
</Alert>
{/snippet}
</AsyncContent>
-->
```
---
## ♿ Pattern 8: Accessible Modal
Focus trap and keyboard handling.
```svelte
<!-- Modal.svelte -->
<script lang="ts">
import type { Snippet } from 'svelte';
import { tick } from 'svelte';
interface Props {
open: boolean;
onclose: () => void;
title: string;
children: Snippet;
footer?: Snippet;
}
let { open, onclose, title, children, footer }: Props = $props();
let dialog: HTMLDialogElement;
let previousFocus: HTMLElement | null = null;
$effect(() => {
if (open) {
previousFocus = document.activeElement as HTMLElement;
tick().then(() => dialog?.showModal());
} else {
dialog?.close();
previousFocus?.focus();
}
});
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') {
onclose();
}
}
function handleBackdropClick(e: MouseEvent) {
if (e.target === dialog) {
onclose();
}
}
</script>
{#if open}
<dialog
bind:this={dialog}
onkeydown={handleKeydown}
onclick={handleBackdropClick}
aria-labelledby="modal-title"
>
<div class="modal-content">
<header>
<h2 id="modal-title">{title}</h2>
<button
class="close-button"
onclick={onclose}
aria-label="Close modal"
>
×
</button>
</header>
<div class="modal-body">
{@render children()}
</div>
{#if footer}
<footer>
{@render footer()}
</footer>
{/if}
</div>
</dialog>
{/if}
<style>
dialog {
padding: 0;
border: none;
border-radius: 0.5rem;
box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25);
}
dialog::backdrop {
background: rgb(0 0 0 / 0.5);
}
</style>
```
+626
View File
@@ -0,0 +1,626 @@
# Svelte 5 State Management Guide
## Overview
Svelte 5 provides multiple patterns for state management, from local component state to global application state. Choose the right pattern based on your needs.
---
## 📊 State Management Decision Tree
```
Is the state used by only one component?
├── YES → Use local $state()
└── NO → Is it shared between parent/child?
├── YES → Pass as props or use $bindable()
└── NO → Is it shared across unrelated components?
├── YES → Use a .svelte.ts store
└── NO → Is it server data that needs caching?
└── YES → Use SvelteKit load functions
```
---
## 🏠 Pattern 1: Local Component State
For state that belongs to a single component.
```svelte
<!-- Counter.svelte -->
<script lang="ts">
let count = $state(0);
let history = $state<number[]>([]);
let average = $derived(
history.length > 0
? history.reduce((a, b) => a + b, 0) / history.length
: 0
);
function increment() {
count++;
history.push(count);
}
function reset() {
count = 0;
history = [];
}
</script>
<div>
<p>Count: {count}</p>
<p>Average: {average.toFixed(2)}</p>
<button onclick={increment}>+1</button>
<button onclick={reset}>Reset</button>
</div>
```
---
## 📤 Pattern 2: Props Down, Events Up
Classic unidirectional data flow.
```svelte
<!-- Parent.svelte -->
<script lang="ts">
import Child from './Child.svelte';
let items = $state(['Apple', 'Banana', 'Cherry']);
function addItem(item: string) {
items.push(item);
}
function removeItem(index: number) {
items.splice(index, 1);
}
</script>
<Child
{items}
onadd={addItem}
onremove={removeItem}
/>
<!-- Child.svelte -->
<script lang="ts">
interface Props {
items: string[];
onadd: (item: string) => void;
onremove: (index: number) => void;
}
let { items, onadd, onremove }: Props = $props();
let newItem = $state('');
function handleAdd() {
if (newItem.trim()) {
onadd(newItem);
newItem = '';
}
}
</script>
<div>
<input bind:value={newItem} placeholder="Add item" />
<button onclick={handleAdd}>Add</button>
<ul>
{#each items as item, i (i)}
<li>
{item}
<button onclick={() => onremove(i)}>×</button>
</li>
{/each}
</ul>
</div>
```
---
## 🔗 Pattern 3: Two-Way Binding with $bindable
For controlled form components.
```svelte
<!-- SearchInput.svelte -->
<script lang="ts">
interface Props {
value?: string;
placeholder?: string;
onsearch?: (query: string) => void;
}
let {
value = $bindable(''),
placeholder = 'Search...',
onsearch
}: Props = $props();
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Enter') {
onsearch?.(value);
}
}
</script>
<input
type="search"
bind:value
{placeholder}
onkeydown={handleKeydown}
/>
<!-- Usage -->
<!--
<script lang="ts">
let searchQuery = $state('');
function handleSearch(query: string) {
console.log('Searching:', query);
}
</script>
<SearchInput bind:value={searchQuery} onsearch={handleSearch} />
<p>Current query: {searchQuery}</p>
-->
```
---
## 🏪 Pattern 4: Global Stores (.svelte.ts files)
For state shared across unrelated components.
```typescript
// stores/auth.svelte.ts
export interface User {
id: string;
name: string;
email: string;
avatar?: string;
}
class AuthStore {
user = $state<User | null>(null);
loading = $state(false);
error = $state<string | null>(null);
isAuthenticated = $derived(this.user !== null);
async login(email: string, password: string) {
this.loading = true;
this.error = null;
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
if (!response.ok) {
throw new Error('Login failed');
}
this.user = await response.json();
} catch (e) {
this.error = e instanceof Error ? e.message : 'Unknown error';
throw e;
} finally {
this.loading = false;
}
}
async logout() {
await fetch('/api/auth/logout', { method: 'POST' });
this.user = null;
}
async checkSession() {
try {
const response = await fetch('/api/auth/me');
if (response.ok) {
this.user = await response.json();
}
} catch {
this.user = null;
}
}
}
export const auth = new AuthStore();
```
```svelte
<!-- NavBar.svelte -->
<script lang="ts">
import { auth } from '$lib/stores/auth.svelte';
</script>
<nav>
{#if auth.isAuthenticated}
<span>Welcome, {auth.user?.name}</span>
<button onclick={() => auth.logout()}>Logout</button>
{:else}
<a href="/login">Login</a>
{/if}
</nav>
```
---
## 🛒 Pattern 5: Complex Store with Actions
For stores with complex business logic.
```typescript
// stores/cart.svelte.ts
export interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
}
class CartStore {
items = $state<CartItem[]>([]);
// Derived values
itemCount = $derived(
this.items.reduce((sum, item) => sum + item.quantity, 0)
);
subtotal = $derived(
this.items.reduce((sum, item) => sum + item.price * item.quantity, 0)
);
tax = $derived(this.subtotal * 0.1);
total = $derived(this.subtotal + this.tax);
isEmpty = $derived(this.items.length === 0);
// Actions
addItem(product: { id: string; name: string; price: number }) {
const existing = this.items.find(item => item.id === product.id);
if (existing) {
existing.quantity++;
} else {
this.items.push({
...product,
quantity: 1
});
}
this.persist();
}
removeItem(id: string) {
const index = this.items.findIndex(item => item.id === id);
if (index !== -1) {
this.items.splice(index, 1);
this.persist();
}
}
updateQuantity(id: string, quantity: number) {
const item = this.items.find(item => item.id === id);
if (item) {
if (quantity <= 0) {
this.removeItem(id);
} else {
item.quantity = quantity;
this.persist();
}
}
}
clear() {
this.items = [];
this.persist();
}
// Persistence
private persist() {
if (typeof localStorage !== 'undefined') {
localStorage.setItem('cart', JSON.stringify($state.snapshot(this.items)));
}
}
hydrate() {
if (typeof localStorage !== 'undefined') {
const saved = localStorage.getItem('cart');
if (saved) {
try {
this.items = JSON.parse(saved);
} catch {
this.items = [];
}
}
}
}
}
export const cart = new CartStore();
```
```svelte
<!-- CartButton.svelte -->
<script lang="ts">
import { cart } from '$lib/stores/cart.svelte';
</script>
<button class="cart-button">
🛒
{#if cart.itemCount > 0}
<span class="badge">{cart.itemCount}</span>
{/if}
</button>
<!-- CartSummary.svelte -->
<script lang="ts">
import { cart } from '$lib/stores/cart.svelte';
</script>
<div class="cart-summary">
<p>Subtotal: ${cart.subtotal.toFixed(2)}</p>
<p>Tax: ${cart.tax.toFixed(2)}</p>
<p><strong>Total: ${cart.total.toFixed(2)}</strong></p>
</div>
```
---
## 🌐 Pattern 6: Context for Component Trees
Pass state down a component tree without prop drilling.
```svelte
<!-- ThemeProvider.svelte -->
<script lang="ts" module>
import { setContext, getContext } from 'svelte';
const THEME_KEY = Symbol('theme');
export type Theme = 'light' | 'dark' | 'system';
export interface ThemeContext {
theme: { value: Theme };
resolvedTheme: { value: 'light' | 'dark' };
setTheme: (theme: Theme) => void;
}
export function getTheme(): ThemeContext {
return getContext(THEME_KEY);
}
</script>
<script lang="ts">
import type { Snippet } from 'svelte';
import { browser } from '$app/environment';
interface Props {
defaultTheme?: Theme;
children: Snippet;
}
let { defaultTheme = 'system', children }: Props = $props();
let theme = $state<Theme>(defaultTheme);
let systemTheme = $state<'light' | 'dark'>('light');
let resolvedTheme = $derived<'light' | 'dark'>(
theme === 'system' ? systemTheme : theme
);
// Watch system preference
$effect(() => {
if (!browser) return;
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
systemTheme = mediaQuery.matches ? 'dark' : 'light';
const handler = (e: MediaQueryListEvent) => {
systemTheme = e.matches ? 'dark' : 'light';
};
mediaQuery.addEventListener('change', handler);
return () => mediaQuery.removeEventListener('change', handler);
});
// Apply theme to document
$effect(() => {
if (!browser) return;
document.documentElement.dataset.theme = resolvedTheme;
});
setContext<ThemeContext>(THEME_KEY, {
get theme() { return { value: theme }; },
get resolvedTheme() { return { value: resolvedTheme }; },
setTheme: (t: Theme) => { theme = t; }
});
</script>
{@render children()}
```
```svelte
<!-- ThemeToggle.svelte -->
<script lang="ts">
import { getTheme } from './ThemeProvider.svelte';
const { theme, setTheme } = getTheme();
</script>
<button onclick={() => setTheme(theme.value === 'dark' ? 'light' : 'dark')}>
{theme.value === 'dark' ? '☀️' : '🌙'}
</button>
```
---
## 📡 Pattern 7: Async State with Loading/Error
Handle async operations elegantly.
```typescript
// stores/async.svelte.ts
export type AsyncState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error };
export function createAsyncState<T>() {
let state = $state<AsyncState<T>>({ status: 'idle' });
return {
get state() { return state; },
get isLoading() { return state.status === 'loading'; },
get isSuccess() { return state.status === 'success'; },
get isError() { return state.status === 'error'; },
get data() { return state.status === 'success' ? state.data : null; },
get error() { return state.status === 'error' ? state.error : null; },
async run(fn: () => Promise<T>) {
state = { status: 'loading' };
try {
const data = await fn();
state = { status: 'success', data };
return data;
} catch (e) {
const error = e instanceof Error ? e : new Error(String(e));
state = { status: 'error', error };
throw error;
}
},
reset() {
state = { status: 'idle' };
}
};
}
// Usage in a store
class UserStore {
private usersAsync = createAsyncState<User[]>();
get users() { return this.usersAsync.data ?? []; }
get loading() { return this.usersAsync.isLoading; }
get error() { return this.usersAsync.error; }
async fetchUsers() {
await this.usersAsync.run(async () => {
const res = await fetch('/api/users');
return res.json();
});
}
}
```
---
## 🔄 Pattern 8: Undo/Redo State
Maintain history for undo/redo functionality.
```typescript
// stores/history.svelte.ts
export function createHistoryState<T>(initial: T) {
let past = $state<T[]>([]);
let present = $state<T>(initial);
let future = $state<T[]>([]);
return {
get value() { return present; },
get canUndo() { return past.length > 0; },
get canRedo() { return future.length > 0; },
set(value: T) {
past.push($state.snapshot(present) as T);
present = value;
future = [];
},
undo() {
if (past.length === 0) return;
future.unshift($state.snapshot(present) as T);
present = past.pop()!;
},
redo() {
if (future.length === 0) return;
past.push($state.snapshot(present) as T);
present = future.shift()!;
},
reset(value: T) {
past = [];
present = value;
future = [];
}
};
}
// Usage
const editor = createHistoryState({ text: '', fontSize: 16 });
// Make changes
editor.set({ text: 'Hello', fontSize: 16 });
editor.set({ text: 'Hello World', fontSize: 16 });
// Undo
editor.undo(); // Back to 'Hello'
editor.undo(); // Back to ''
// Redo
editor.redo(); // Forward to 'Hello'
```
---
## 📋 State Management Best Practices
### ✅ Do:
- Keep state as local as possible
- Use `$derived` for computed values
- Use `.svelte.ts` files for shared state
- Type your state with TypeScript
- Use `$state.snapshot()` before serializing
- Clean up effects that create subscriptions
### ❌ Don't:
- Create global stores for component-local state
- Mutate derived values
- Put async logic in `$effect` without cleanup
- Destructure `$state` objects (breaks reactivity)
- Forget to handle loading/error states
---
## 🔍 Debugging State
```svelte
<script lang="ts">
let user = $state({ name: 'John', age: 30 });
let items = $state([1, 2, 3]);
// Log all changes during development
$inspect(user, items);
// Or with custom handler
$inspect(user).with((type, ...values) => {
if (type === 'update') {
console.table(values);
}
});
</script>
```
+728
View File
@@ -0,0 +1,728 @@
# SvelteKit Routing & Data Loading Guide
## Overview
SvelteKit is the official framework for building Svelte applications. It provides file-based routing, server-side rendering, and powerful data loading primitives.
---
## 📁 File-Based Routing
### Route Structure
```
src/routes/
├── +page.svelte # → /
├── +layout.svelte # Layout for all routes
├── about/
│ └── +page.svelte # → /about
├── blog/
│ ├── +page.svelte # → /blog
│ └── [slug]/
│ └── +page.svelte # → /blog/:slug
├── users/
│ ├── +page.svelte # → /users
│ ├── [id]/
│ │ ├── +page.svelte # → /users/:id
│ │ └── +page.server.ts # Server-only load
│ └── [...rest]/
│ └── +page.svelte # → /users/* (catch-all)
└── api/
└── posts/
└── +server.ts # → /api/posts (API endpoint)
```
### Route Files Explained
| File | Purpose | Runs On |
|------|---------|---------|
| `+page.svelte` | Page component | Client |
| `+page.ts` | Load data for page | Client & Server |
| `+page.server.ts` | Load data (server only) | Server only |
| `+layout.svelte` | Shared layout | Client |
| `+layout.ts` | Load data for layout | Client & Server |
| `+layout.server.ts` | Load layout data (server) | Server only |
| `+server.ts` | API endpoint | Server only |
| `+error.svelte` | Error page | Client |
---
## 📥 Data Loading
### Basic Load Function (+page.ts)
```typescript
// src/routes/posts/+page.ts
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ fetch }) => {
const response = await fetch('/api/posts');
const posts = await response.json();
return {
posts
};
};
```
```svelte
<!-- src/routes/posts/+page.svelte -->
<script lang="ts">
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
</script>
<h1>Posts</h1>
{#each data.posts as post}
<article>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
{/each}
```
### Server-Only Load (+page.server.ts)
```typescript
// src/routes/dashboard/+page.server.ts
import type { PageServerLoad } from './$types';
import { db } from '$lib/server/database';
import { error, redirect } from '@sveltejs/kit';
export const load: PageServerLoad = async ({ locals, cookies }) => {
// Access server-only resources
const session = cookies.get('session');
if (!session) {
redirect(303, '/login');
}
const user = await db.user.findUnique({
where: { sessionId: session }
});
if (!user) {
error(401, 'Unauthorized');
}
const stats = await db.stats.findMany({
where: { userId: user.id }
});
return {
user: {
name: user.name,
email: user.email
},
stats
};
};
```
### Load with Parameters
```typescript
// src/routes/blog/[slug]/+page.ts
import type { PageLoad } from './$types';
import { error } from '@sveltejs/kit';
export const load: PageLoad = async ({ params, fetch }) => {
const response = await fetch(`/api/posts/${params.slug}`);
if (!response.ok) {
error(404, 'Post not found');
}
const post = await response.json();
return {
post
};
};
```
### Parent Data Access
```typescript
// src/routes/+layout.ts
import type { LayoutLoad } from './$types';
export const load: LayoutLoad = async ({ fetch }) => {
const response = await fetch('/api/user');
const user = response.ok ? await response.json() : null;
return { user };
};
// src/routes/dashboard/+page.ts
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ parent }) => {
// Access data from parent layout
const { user } = await parent();
if (!user) {
// Handle unauthenticated state
}
return {
// Additional data
};
};
```
### Parallel Data Loading
```typescript
// src/routes/dashboard/+page.ts
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ fetch }) => {
// Load data in parallel
const [postsRes, statsRes, notificationsRes] = await Promise.all([
fetch('/api/posts'),
fetch('/api/stats'),
fetch('/api/notifications')
]);
return {
posts: postsRes.json(),
stats: statsRes.json(),
notifications: notificationsRes.json()
};
};
```
---
## 📤 Form Actions
### Basic Form Action
```typescript
// src/routes/login/+page.server.ts
import type { Actions, PageServerLoad } from './$types';
import { fail, redirect } from '@sveltejs/kit';
import { db } from '$lib/server/database';
export const load: PageServerLoad = async ({ locals }) => {
if (locals.user) {
redirect(303, '/dashboard');
}
};
export const actions: Actions = {
default: async ({ request, cookies }) => {
const data = await request.formData();
const email = data.get('email')?.toString();
const password = data.get('password')?.toString();
if (!email || !password) {
return fail(400, {
email,
error: 'Email and password are required'
});
}
const user = await db.user.findUnique({ where: { email } });
if (!user || !await verifyPassword(password, user.passwordHash)) {
return fail(401, {
email,
error: 'Invalid credentials'
});
}
const session = await createSession(user.id);
cookies.set('session', session.id, {
path: '/',
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7 // 1 week
});
redirect(303, '/dashboard');
}
};
```
```svelte
<!-- src/routes/login/+page.svelte -->
<script lang="ts">
import type { ActionData, PageData } from './$types';
import { enhance } from '$app/forms';
let { form }: { form: ActionData } = $props();
let loading = $state(false);
</script>
<form
method="POST"
use:enhance={() => {
loading = true;
return async ({ update }) => {
loading = false;
await update();
};
}}
>
{#if form?.error}
<p class="error">{form.error}</p>
{/if}
<label>
Email
<input
type="email"
name="email"
value={form?.email ?? ''}
required
/>
</label>
<label>
Password
<input type="password" name="password" required />
</label>
<button type="submit" disabled={loading}>
{loading ? 'Logging in...' : 'Login'}
</button>
</form>
```
### Named Actions
```typescript
// src/routes/settings/+page.server.ts
import type { Actions } from './$types';
export const actions: Actions = {
updateProfile: async ({ request }) => {
const data = await request.formData();
// Handle profile update
return { success: true };
},
changePassword: async ({ request }) => {
const data = await request.formData();
// Handle password change
return { success: true };
},
deleteAccount: async ({ request, cookies }) => {
// Handle account deletion
cookies.delete('session', { path: '/' });
redirect(303, '/');
}
};
```
```svelte
<!-- Use named actions -->
<form method="POST" action="?/updateProfile" use:enhance>
<!-- Profile form -->
</form>
<form method="POST" action="?/changePassword" use:enhance>
<!-- Password form -->
</form>
<form method="POST" action="?/deleteAccount" use:enhance>
<button type="submit">Delete Account</button>
</form>
```
---
## 🔌 API Routes (+server.ts)
### RESTful API Endpoint
```typescript
// src/routes/api/posts/+server.ts
import type { RequestHandler } from './$types';
import { json, error } from '@sveltejs/kit';
import { db } from '$lib/server/database';
// GET /api/posts
export const GET: RequestHandler = async ({ url }) => {
const limit = Number(url.searchParams.get('limit')) || 10;
const offset = Number(url.searchParams.get('offset')) || 0;
const posts = await db.post.findMany({
take: limit,
skip: offset,
orderBy: { createdAt: 'desc' }
});
return json(posts);
};
// POST /api/posts
export const POST: RequestHandler = async ({ request, locals }) => {
if (!locals.user) {
error(401, 'Unauthorized');
}
const body = await request.json();
const post = await db.post.create({
data: {
title: body.title,
content: body.content,
authorId: locals.user.id
}
});
return json(post, { status: 201 });
};
// src/routes/api/posts/[id]/+server.ts
import type { RequestHandler } from './$types';
import { json, error } from '@sveltejs/kit';
// GET /api/posts/:id
export const GET: RequestHandler = async ({ params }) => {
const post = await db.post.findUnique({
where: { id: params.id }
});
if (!post) {
error(404, 'Post not found');
}
return json(post);
};
// PUT /api/posts/:id
export const PUT: RequestHandler = async ({ params, request, locals }) => {
const post = await db.post.findUnique({
where: { id: params.id }
});
if (!post) {
error(404, 'Post not found');
}
if (post.authorId !== locals.user?.id) {
error(403, 'Forbidden');
}
const body = await request.json();
const updated = await db.post.update({
where: { id: params.id },
data: body
});
return json(updated);
};
// DELETE /api/posts/:id
export const DELETE: RequestHandler = async ({ params, locals }) => {
const post = await db.post.findUnique({
where: { id: params.id }
});
if (!post) {
error(404, 'Post not found');
}
if (post.authorId !== locals.user?.id) {
error(403, 'Forbidden');
}
await db.post.delete({
where: { id: params.id }
});
return new Response(null, { status: 204 });
};
```
---
## 🏗️ Layouts
### Root Layout
```svelte
<!-- src/routes/+layout.svelte -->
<script lang="ts">
import type { LayoutData } from './$types';
import type { Snippet } from 'svelte';
import Header from '$lib/components/Header.svelte';
import Footer from '$lib/components/Footer.svelte';
import '../app.css';
let { data, children }: { data: LayoutData; children: Snippet } = $props();
</script>
<div class="app">
<Header user={data.user} />
<main>
{@render children()}
</main>
<Footer />
</div>
```
### Nested Layouts
```svelte
<!-- src/routes/dashboard/+layout.svelte -->
<script lang="ts">
import type { LayoutData } from './$types';
import type { Snippet } from 'svelte';
import Sidebar from '$lib/components/Sidebar.svelte';
let { data, children }: { data: LayoutData; children: Snippet } = $props();
</script>
<div class="dashboard">
<Sidebar />
<div class="content">
{@render children()}
</div>
</div>
```
### Layout Groups
```
src/routes/
├── (auth)/ # Group - no route segment
│ ├── +layout.svelte # Shared layout for auth pages
│ ├── login/
│ │ └── +page.svelte
│ └── register/
│ └── +page.svelte
├── (app)/ # Group - no route segment
│ ├── +layout.svelte # Shared layout for app pages
│ ├── dashboard/
│ │ └── +page.svelte
│ └── settings/
│ └── +page.svelte
└── +layout.svelte # Root layout
```
### Breaking Out of Layout
```svelte
<!-- src/routes/special/+page@.svelte -->
<!-- The @. means use root layout, not parent -->
<script lang="ts">
// This page uses +layout.svelte directly, skipping any nested layouts
</script>
```
---
## 🔀 Navigation
### Programmatic Navigation
```svelte
<script lang="ts">
import { goto, invalidate, invalidateAll } from '$app/navigation';
import { page } from '$app/stores';
// Navigate to a page
async function navigate() {
await goto('/dashboard');
}
// Navigate with options
async function navigateWithState() {
await goto('/search', {
replaceState: true, // Replace instead of push
keepFocus: true, // Keep focus on current element
noScroll: true, // Don't scroll to top
state: { query: 'test' } // Pass state
});
}
// Invalidate load functions
async function refresh() {
await invalidate('/api/posts'); // Rerun loads that depend on this
// or
await invalidateAll(); // Rerun all load functions
}
</script>
<!-- Current page info -->
<p>Current path: {$page.url.pathname}</p>
<p>Query: {$page.url.searchParams.get('q')}</p>
```
### Link Prefetching
```svelte
<!-- Preload on hover (default) -->
<a href="/about">About</a>
<!-- Preload on viewport entry -->
<a href="/posts" data-sveltekit-preload-data="hover">Posts</a>
<!-- Disable preloading -->
<a href="/external" data-sveltekit-preload-data="off">External</a>
<!-- Preload immediately when link is rendered -->
<a href="/dashboard" data-sveltekit-preload-data="eager">Dashboard</a>
```
---
## ⚠️ Error Handling
### Error Pages
```svelte
<!-- src/routes/+error.svelte -->
<script lang="ts">
import { page } from '$app/stores';
</script>
<div class="error">
<h1>{$page.status}</h1>
<p>{$page.error?.message}</p>
<a href="/">Go home</a>
</div>
<!-- src/routes/blog/+error.svelte -->
<!-- More specific error page for /blog routes -->
<script lang="ts">
import { page } from '$app/stores';
</script>
<div class="blog-error">
<h1>Blog Post Not Found</h1>
<p>{$page.error?.message}</p>
<a href="/blog">View all posts</a>
</div>
```
### Throwing Errors
```typescript
// In load functions or actions
import { error, redirect } from '@sveltejs/kit';
export const load = async ({ params }) => {
const post = await getPost(params.id);
if (!post) {
error(404, {
message: 'Post not found',
code: 'POST_NOT_FOUND'
});
}
if (post.draft) {
redirect(303, '/blog');
}
return { post };
};
```
---
## 🔧 Hooks
### Server Hooks (hooks.server.ts)
```typescript
// src/hooks.server.ts
import type { Handle, HandleFetch, HandleServerError } from '@sveltejs/kit';
// Runs for every request
export const handle: Handle = async ({ event, resolve }) => {
// Authentication
const session = event.cookies.get('session');
if (session) {
event.locals.user = await getUserFromSession(session);
}
// Performance timing
const start = Date.now();
const response = await resolve(event, {
// Transform HTML
transformPageChunk: ({ html }) => html.replace('%theme%', 'dark')
});
// Log request duration
console.log(`${event.request.method} ${event.url.pathname} - ${Date.now() - start}ms`);
return response;
};
// Customize server-side fetch
export const handleFetch: HandleFetch = async ({ request, fetch }) => {
// Add auth header to internal API calls
if (request.url.startsWith('https://api.internal.com')) {
request.headers.set('Authorization', `Bearer ${API_KEY}`);
}
return fetch(request);
};
// Handle unexpected errors
export const handleError: HandleServerError = async ({ error, event }) => {
console.error('Server error:', error);
// Report to error tracking service
await reportError(error, event);
return {
message: 'An unexpected error occurred',
code: 'INTERNAL_ERROR'
};
};
```
### Client Hooks (hooks.client.ts)
```typescript
// src/hooks.client.ts
import type { HandleClientError } from '@sveltejs/kit';
export const handleError: HandleClientError = async ({ error, event }) => {
console.error('Client error:', error);
// Report to error tracking
await reportClientError(error);
return {
message: 'Something went wrong',
code: 'CLIENT_ERROR'
};
};
```
---
## 📋 Best Practices
### ✅ Do:
- Use `+page.server.ts` for sensitive data
- Return typed data from load functions
- Use `use:enhance` for progressive enhancement
- Handle loading and error states
- Use route groups for layout organization
- Validate form data server-side
### ❌ Don't:
- Put secrets in `+page.ts` (runs on client)
- Forget to await `parent()` in child loads
- Block navigation with slow loads
- Ignore accessibility in forms
- Hardcode URLs (use `$page` store)
+845
View File
@@ -0,0 +1,845 @@
# Svelte + TypeScript Complete Guide
## Overview
TypeScript is a first-class citizen in Svelte 5. This guide covers all TypeScript patterns for Svelte development.
---
## 🔧 Configuration
### tsconfig.json
```json
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"strict": true,
"moduleResolution": "bundler",
"module": "ESNext",
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"allowImportingTsExtensions": true,
"noEmit": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"skipLibCheck": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"exactOptionalPropertyTypes": true,
"noUncheckedIndexedAccess": true
},
"include": [
".svelte-kit/**/*.ts",
"src/**/*.ts",
"src/**/*.svelte"
]
}
```
### svelte.config.js
```javascript
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
preprocess: vitePreprocess(),
kit: {
adapter: adapter(),
alias: {
$components: './src/lib/components',
$stores: './src/lib/stores',
$utils: './src/lib/utils',
$types: './src/lib/types'
}
}
};
export default config;
```
---
## 📦 Component Typing
### Basic Props
```svelte
<script lang="ts">
interface Props {
name: string;
age: number;
email?: string;
active?: boolean;
}
let {
name,
age,
email = 'N/A',
active = true
}: Props = $props();
</script>
```
### Props with HTML Attributes
```svelte
<script lang="ts">
import type { HTMLButtonAttributes } from 'svelte/elements';
interface Props extends HTMLButtonAttributes {
variant?: 'primary' | 'secondary' | 'danger';
loading?: boolean;
}
let {
variant = 'primary',
loading = false,
disabled,
...rest
}: Props = $props();
let isDisabled = $derived(disabled || loading);
</script>
<button class={variant} disabled={isDisabled} {...rest}>
{#if loading}
<span class="spinner" />
{/if}
<slot />
</button>
```
### Props with Snippets
```svelte
<script lang="ts">
import type { Snippet } from 'svelte';
interface Props {
children: Snippet;
header?: Snippet;
footer?: Snippet<[{ close: () => void }]>;
}
let { children, header, footer }: Props = $props();
let open = $state(true);
function close() {
open = false;
}
</script>
{#if open}
<div class="modal">
{#if header}
{@render header()}
{/if}
<div class="body">
{@render children()}
</div>
{#if footer}
{@render footer({ close })}
{/if}
</div>
{/if}
```
### Generic Components
```svelte
<script lang="ts" generics="T extends { id: string | number }">
import type { Snippet } from 'svelte';
interface Props {
items: T[];
selected?: T | null;
renderItem: Snippet<[T]>;
onselect?: (item: T) => void;
}
let { items, selected = null, renderItem, onselect }: Props = $props();
</script>
<ul>
{#each items as item (item.id)}
<li
class:selected={selected?.id === item.id}
onclick={() => onselect?.(item)}
>
{@render renderItem(item)}
</li>
{/each}
</ul>
```
---
## 🔗 Event Typing
### Custom Event Handlers
```svelte
<script lang="ts">
interface Props {
onchange?: (value: string) => void;
onsubmit?: (data: FormData) => void;
onclear?: () => void;
}
let { onchange, onsubmit, onclear }: Props = $props();
let value = $state('');
function handleInput(e: Event) {
const target = e.target as HTMLInputElement;
value = target.value;
onchange?.(value);
}
function handleSubmit(e: SubmitEvent) {
e.preventDefault();
const formData = new FormData(e.currentTarget as HTMLFormElement);
onsubmit?.(formData);
}
</script>
<form onsubmit={handleSubmit}>
<input type="text" oninput={handleInput} />
<button type="submit">Submit</button>
<button type="button" onclick={onclear}>Clear</button>
</form>
```
### DOM Event Types
```svelte
<script lang="ts">
// Common event types
function handleClick(e: MouseEvent) {
console.log(e.clientX, e.clientY);
}
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Enter') {
// Handle enter
}
}
function handleInput(e: Event & { currentTarget: HTMLInputElement }) {
console.log(e.currentTarget.value);
}
function handleChange(e: Event & { currentTarget: HTMLSelectElement }) {
console.log(e.currentTarget.value);
}
function handleSubmit(e: SubmitEvent) {
e.preventDefault();
const form = e.currentTarget as HTMLFormElement;
const data = new FormData(form);
}
function handleFocus(e: FocusEvent) {
// Handle focus
}
function handleDrag(e: DragEvent) {
// Handle drag
}
</script>
<button onclick={handleClick}>Click me</button>
<input oninput={handleInput} onkeydown={handleKeydown} />
<form onsubmit={handleSubmit}>...</form>
```
---
## 📊 State Typing
### Basic State
```svelte
<script lang="ts">
// Primitive types are inferred
let count = $state(0); // number
let name = $state(''); // string
let active = $state(false); // boolean
// Explicit typing for complex types
let user = $state<User | null>(null);
let items = $state<string[]>([]);
let map = $state<Map<string, number>>(new Map());
// Typing nullable state
let data = $state<ApiResponse | undefined>(undefined);
</script>
```
### Derived Types
```svelte
<script lang="ts">
interface User {
id: string;
name: string;
roles: string[];
}
let users = $state<User[]>([]);
let searchQuery = $state('');
// Type is inferred from expression
let filteredUsers = $derived(
users.filter(u =>
u.name.toLowerCase().includes(searchQuery.toLowerCase())
)
);
// Explicit return type with $derived.by
let stats = $derived.by((): { total: number; admins: number } => {
return {
total: users.length,
admins: users.filter(u => u.roles.includes('admin')).length
};
});
</script>
```
### Effect Types
```svelte
<script lang="ts">
let count = $state(0);
// Effect with cleanup
$effect(() => {
const controller = new AbortController();
fetchData(controller.signal)
.then(handleData)
.catch(handleError);
// Cleanup function
return () => {
controller.abort();
};
});
// Pre-effect
let container: HTMLDivElement;
$effect.pre(() => {
// Runs before DOM updates
if (container) {
// Save scroll position
}
});
</script>
```
---
## 🏪 Store Types (.svelte.ts)
### Class-Based Store
```typescript
// src/lib/stores/counter.svelte.ts
export class Counter {
count = $state(0);
doubled = $derived(this.count * 2);
increment() {
this.count++;
}
decrement() {
this.count--;
}
reset() {
this.count = 0;
}
}
// Singleton instance
export const counter = new Counter();
```
### Complex Typed Store
```typescript
// src/lib/stores/auth.svelte.ts
export interface User {
id: string;
email: string;
name: string;
avatar?: string;
roles: ('user' | 'admin' | 'moderator')[];
}
export interface AuthState {
user: User | null;
loading: boolean;
error: string | null;
}
class AuthStore {
user = $state<User | null>(null);
loading = $state(false);
error = $state<string | null>(null);
isAuthenticated = $derived(this.user !== null);
isAdmin = $derived(this.user?.roles.includes('admin') ?? false);
async login(email: string, password: string): Promise<void> {
this.loading = true;
this.error = null;
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.message || 'Login failed');
}
this.user = await response.json();
} catch (e) {
this.error = e instanceof Error ? e.message : 'Unknown error';
throw e;
} finally {
this.loading = false;
}
}
logout(): void {
this.user = null;
}
}
export const auth = new AuthStore();
```
### Factory Function Store
```typescript
// src/lib/stores/createPagination.svelte.ts
export interface PaginationOptions {
initialPage?: number;
pageSize?: number;
}
export interface PaginationState<T> {
items: T[];
page: number;
pageSize: number;
total: number;
}
export function createPagination<T>(
fetchFn: (page: number, pageSize: number) => Promise<{ items: T[]; total: number }>,
options: PaginationOptions = {}
) {
const { initialPage = 1, pageSize = 10 } = options;
let items = $state<T[]>([]);
let page = $state(initialPage);
let total = $state(0);
let loading = $state(false);
let error = $state<Error | null>(null);
const totalPages = $derived(Math.ceil(total / pageSize));
const hasNext = $derived(page < totalPages);
const hasPrev = $derived(page > 1);
async function load() {
loading = true;
error = null;
try {
const result = await fetchFn(page, pageSize);
items = result.items;
total = result.total;
} catch (e) {
error = e instanceof Error ? e : new Error(String(e));
} finally {
loading = false;
}
}
function nextPage() {
if (hasNext) {
page++;
load();
}
}
function prevPage() {
if (hasPrev) {
page--;
load();
}
}
function goToPage(p: number) {
if (p >= 1 && p <= totalPages) {
page = p;
load();
}
}
return {
get items() { return items; },
get page() { return page; },
get pageSize() { return pageSize; },
get total() { return total; },
get totalPages() { return totalPages; },
get loading() { return loading; },
get error() { return error; },
get hasNext() { return hasNext; },
get hasPrev() { return hasPrev; },
load,
nextPage,
prevPage,
goToPage
};
}
```
---
## 🌐 SvelteKit Types
### Load Functions
```typescript
// src/routes/users/[id]/+page.ts
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ params, fetch, parent }) => {
// params is typed based on route
const userId = params.id; // string
// parent() returns parent layout data
const parentData = await parent();
const response = await fetch(`/api/users/${userId}`);
const user = await response.json() as User;
return {
user,
parentData
};
};
```
### Server Load Functions
```typescript
// src/routes/admin/+page.server.ts
import type { PageServerLoad } from './$types';
import { error, redirect } from '@sveltejs/kit';
export const load: PageServerLoad = async ({ locals, cookies }) => {
// locals is typed in app.d.ts
if (!locals.user) {
redirect(303, '/login');
}
if (!locals.user.roles.includes('admin')) {
error(403, 'Forbidden');
}
// Access cookies
const theme = cookies.get('theme') ?? 'light';
return {
user: locals.user,
theme
};
};
```
### Form Actions
```typescript
// src/routes/posts/new/+page.server.ts
import type { Actions } from './$types';
import { fail, redirect } from '@sveltejs/kit';
export const actions: Actions = {
default: async ({ request, locals }) => {
const data = await request.formData();
const title = data.get('title')?.toString();
const content = data.get('content')?.toString();
if (!title || !content) {
return fail(400, {
title,
content,
error: 'Title and content are required'
});
}
const post = await createPost({
title,
content,
authorId: locals.user!.id
});
redirect(303, `/posts/${post.id}`);
}
};
```
### API Routes
```typescript
// src/routes/api/users/+server.ts
import type { RequestHandler } from './$types';
import { json, error } from '@sveltejs/kit';
export const GET: RequestHandler = async ({ url }) => {
const limit = Number(url.searchParams.get('limit')) || 10;
const users = await getUsers(limit);
return json(users);
};
export const POST: RequestHandler = async ({ request }) => {
const body = await request.json() as CreateUserInput;
// Validate
if (!body.email || !body.name) {
error(400, 'Missing required fields');
}
const user = await createUser(body);
return json(user, { status: 201 });
};
```
### App Types (app.d.ts)
```typescript
// src/app.d.ts
// See https://svelte.dev/docs/kit/types#app.d.ts
declare global {
namespace App {
interface Error {
message: string;
code?: string;
}
interface Locals {
user: {
id: string;
email: string;
name: string;
roles: string[];
} | null;
}
interface PageData {
// Shared page data
}
interface PageState {
// History state
}
interface Platform {
// Platform-specific
}
}
}
export {};
```
---
## 🎯 Utility Types
### Component Types
```typescript
// src/lib/types/components.ts
import type { Snippet } from 'svelte';
import type { HTMLAttributes } from 'svelte/elements';
// Base props for all components
export interface BaseProps {
class?: string;
id?: string;
}
// Polymorphic component props
export type PolymorphicProps<E extends keyof HTMLElementTagNameMap> =
BaseProps & HTMLAttributes<HTMLElementTagNameMap[E]>;
// Common snippet patterns
export type RenderProp<T> = Snippet<[T]>;
export type ChildrenSnippet = Snippet;
// Status union
export type Status = 'idle' | 'loading' | 'success' | 'error';
// Async state
export type AsyncState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error };
```
### API Types
```typescript
// src/lib/types/api.ts
export interface ApiResponse<T> {
data: T;
meta?: {
page: number;
pageSize: number;
total: number;
totalPages: number;
};
}
export interface ApiError {
message: string;
code: string;
details?: Record<string, string[]>;
}
export type ApiResult<T> =
| { success: true; data: T }
| { success: false; error: ApiError };
// Type-safe fetch wrapper
export async function apiFetch<T>(
url: string,
options?: RequestInit
): Promise<T> {
const response = await fetch(url, {
headers: {
'Content-Type': 'application/json',
...options?.headers
},
...options
});
if (!response.ok) {
const error = await response.json() as ApiError;
throw new Error(error.message);
}
return response.json() as Promise<T>;
}
```
---
## 📋 Type Checking Commands
```bash
# Run type checking
npx svelte-check --tsconfig ./tsconfig.json
# Watch mode
npx svelte-check --tsconfig ./tsconfig.json --watch
# With threshold (fail on warnings)
npx svelte-check --tsconfig ./tsconfig.json --fail-on-warnings
# Output format
npx svelte-check --output human
npx svelte-check --output human-verbose
npx svelte-check --output machine
```
### package.json scripts
```json
{
"scripts": {
"check": "svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "eslint . && npm run check"
}
}
```
---
## ⚠️ Common TypeScript Pitfalls
### ❌ Destructuring $state Breaks Reactivity
```svelte
<script lang="ts">
let user = $state({ name: 'John', age: 30 });
// ❌ BAD: Loses reactivity
const { name, age } = user;
// ✅ GOOD: Access directly
// user.name, user.age
</script>
<p>{user.name} is {user.age}</p>
```
### ❌ Forgetting to Type Props
```svelte
<script lang="ts">
// ❌ BAD: No type safety
let { name, age } = $props();
// ✅ GOOD: Typed props
let { name, age }: { name: string; age: number } = $props();
</script>
```
### ❌ Using `any`
```svelte
<script lang="ts">
// ❌ BAD: Defeats TypeScript
let data: any = $state(null);
// ✅ GOOD: Proper typing
interface Data {
id: string;
value: number;
}
let data = $state<Data | null>(null);
</script>
```
+733
View File
@@ -0,0 +1,733 @@
# Svelte Styling & Tailwind Integration
## Overview
Svelte provides scoped CSS by default and integrates seamlessly with Tailwind CSS. This guide covers both approaches and best practices.
---
## 🎨 Scoped CSS (Default)
### Basic Scoped Styles
```svelte
<script lang="ts">
let { variant = 'primary' }: { variant?: 'primary' | 'secondary' } = $props();
</script>
<button class={variant}>
<slot />
</button>
<style>
/* These styles are automatically scoped to this component */
button {
padding: 0.5rem 1rem;
border-radius: 0.25rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.primary {
background: var(--color-primary);
color: white;
border: none;
}
.primary:hover {
background: var(--color-primary-dark);
}
.secondary {
background: transparent;
color: var(--color-primary);
border: 2px solid currentColor;
}
.secondary:hover {
background: var(--color-primary);
color: white;
}
</style>
```
### Global Styles with :global()
```svelte
<style>
/* Scoped to this component */
.container {
padding: 1rem;
}
/* Applies globally - use sparingly */
:global(body) {
margin: 0;
font-family: system-ui;
}
/* Global styles WITHIN a scoped selector */
.container :global(a) {
color: var(--color-link);
}
/* Global class that can be used anywhere */
:global(.sr-only) {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
</style>
```
### CSS Custom Properties (Variables)
```svelte
<!-- Define in root layout or app.css -->
<style>
:root {
/* Colors */
--color-primary: #3b82f6;
--color-primary-dark: #2563eb;
--color-secondary: #6b7280;
--color-success: #10b981;
--color-warning: #f59e0b;
--color-error: #ef4444;
/* Surfaces */
--color-background: #ffffff;
--color-surface: #f9fafb;
--color-border: #e5e7eb;
/* Text */
--color-text: #111827;
--color-text-muted: #6b7280;
/* Spacing */
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 0.75rem;
--space-4: 1rem;
--space-6: 1.5rem;
--space-8: 2rem;
/* Typography */
--font-sans: system-ui, -apple-system, sans-serif;
--font-mono: ui-monospace, monospace;
/* Shadows */
--shadow-sm: 0 1px 2px rgb(0 0 0 / 0.05);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
/* Borders */
--radius-sm: 0.25rem;
--radius-md: 0.375rem;
--radius-lg: 0.5rem;
--radius-full: 9999px;
}
/* Dark mode */
:root[data-theme="dark"] {
--color-background: #111827;
--color-surface: #1f2937;
--color-border: #374151;
--color-text: #f9fafb;
--color-text-muted: #9ca3af;
}
</style>
```
---
## 💨 Tailwind CSS Integration
### Setup
```bash
# Install Tailwind
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
```
### tailwind.config.js
```javascript
/** @type {import('tailwindcss').Config} */
export default {
content: ['./src/**/*.{html,js,svelte,ts}'],
darkMode: 'class', // or 'media'
theme: {
extend: {
colors: {
primary: {
50: '#eff6ff',
100: '#dbeafe',
200: '#bfdbfe',
300: '#93c5fd',
400: '#60a5fa',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
800: '#1e40af',
900: '#1e3a8a',
},
},
fontFamily: {
sans: ['Inter var', 'system-ui', 'sans-serif'],
},
},
},
plugins: [
require('@tailwindcss/forms'),
require('@tailwindcss/typography'),
],
};
```
### postcss.config.js
```javascript
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
```
### app.css
```css
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Custom base styles */
@layer base {
body {
@apply bg-white dark:bg-gray-900 text-gray-900 dark:text-white;
}
h1, h2, h3, h4, h5, h6 {
@apply font-bold tracking-tight;
}
}
/* Custom component classes */
@layer components {
.btn {
@apply inline-flex items-center justify-center px-4 py-2
font-medium rounded-lg transition-colors
focus:outline-none focus:ring-2 focus:ring-offset-2;
}
.btn-primary {
@apply bg-primary-600 text-white hover:bg-primary-700
focus:ring-primary-500;
}
.btn-secondary {
@apply bg-gray-100 text-gray-700 hover:bg-gray-200
dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700;
}
.input {
@apply block w-full rounded-lg border border-gray-300
px-3 py-2 shadow-sm
focus:border-primary-500 focus:ring-1 focus:ring-primary-500
dark:border-gray-600 dark:bg-gray-800;
}
.card {
@apply rounded-xl bg-white dark:bg-gray-800
border border-gray-200 dark:border-gray-700
shadow-sm overflow-hidden;
}
}
/* Custom utilities */
@layer utilities {
.text-balance {
text-wrap: balance;
}
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
}
```
---
## 🧩 Component Styling Patterns
### Pattern 1: Tailwind Component
```svelte
<!-- Button.svelte -->
<script lang="ts">
import type { Snippet } from 'svelte';
import type { HTMLButtonAttributes } from 'svelte/elements';
interface Props extends HTMLButtonAttributes {
variant?: 'primary' | 'secondary' | 'danger' | 'ghost';
size?: 'sm' | 'md' | 'lg';
loading?: boolean;
children: Snippet;
}
let {
variant = 'primary',
size = 'md',
loading = false,
disabled,
class: className = '',
children,
...rest
}: Props = $props();
const baseClasses = `
inline-flex items-center justify-center font-medium rounded-lg
transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2
disabled:opacity-50 disabled:cursor-not-allowed
`;
const variantClasses = {
primary: 'bg-primary-600 text-white hover:bg-primary-700 focus:ring-primary-500',
secondary: 'bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-300',
danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500',
ghost: 'text-gray-600 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800'
};
const sizeClasses = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-sm',
lg: 'px-6 py-3 text-base'
};
let classes = $derived(
`${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]} ${className}`.trim()
);
</script>
<button class={classes} disabled={disabled || loading} {...rest}>
{#if loading}
<svg class="animate-spin -ml-1 mr-2 h-4 w-4" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
{/if}
{@render children()}
</button>
```
### Pattern 2: Class Merging with clsx/tailwind-merge
```bash
npm install clsx tailwind-merge
```
```typescript
// src/lib/utils/cn.ts
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
```
```svelte
<!-- Card.svelte -->
<script lang="ts">
import { cn } from '$lib/utils/cn';
import type { Snippet } from 'svelte';
interface Props {
variant?: 'default' | 'bordered' | 'elevated';
padding?: 'none' | 'sm' | 'md' | 'lg';
class?: string;
children: Snippet;
}
let {
variant = 'default',
padding = 'md',
class: className,
children
}: Props = $props();
</script>
<div
class={cn(
'rounded-xl bg-white dark:bg-gray-800',
{
'border border-gray-200 dark:border-gray-700': variant === 'default' || variant === 'bordered',
'shadow-lg': variant === 'elevated',
'p-0': padding === 'none',
'p-4': padding === 'sm',
'p-6': padding === 'md',
'p-8': padding === 'lg',
},
className
)}
>
{@render children()}
</div>
```
### Pattern 3: CVA (Class Variance Authority)
```bash
npm install class-variance-authority
```
```typescript
// src/lib/components/Button/variants.ts
import { cva, type VariantProps } from 'class-variance-authority';
export const buttonVariants = cva(
// Base classes
'inline-flex items-center justify-center font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed',
{
variants: {
variant: {
primary: 'bg-primary-600 text-white hover:bg-primary-700 focus:ring-primary-500',
secondary: 'bg-gray-100 text-gray-700 hover:bg-gray-200',
danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500',
ghost: 'text-gray-600 hover:bg-gray-100',
link: 'text-primary-600 underline-offset-4 hover:underline'
},
size: {
sm: 'h-8 px-3 text-sm',
md: 'h-10 px-4 text-sm',
lg: 'h-12 px-6 text-base',
icon: 'h-10 w-10'
}
},
defaultVariants: {
variant: 'primary',
size: 'md'
}
}
);
export type ButtonVariants = VariantProps<typeof buttonVariants>;
```
```svelte
<!-- Button.svelte -->
<script lang="ts">
import { buttonVariants, type ButtonVariants } from './variants';
import { cn } from '$lib/utils/cn';
import type { Snippet } from 'svelte';
import type { HTMLButtonAttributes } from 'svelte/elements';
interface Props extends ButtonVariants, HTMLButtonAttributes {
class?: string;
children: Snippet;
}
let { variant, size, class: className, children, ...rest }: Props = $props();
</script>
<button class={cn(buttonVariants({ variant, size }), className)} {...rest}>
{@render children()}
</button>
```
---
## 🌓 Dark Mode
### With Tailwind
```svelte
<!-- ThemeToggle.svelte -->
<script lang="ts">
import { browser } from '$app/environment';
type Theme = 'light' | 'dark' | 'system';
let theme = $state<Theme>('system');
$effect(() => {
if (!browser) return;
const saved = localStorage.getItem('theme') as Theme | null;
if (saved) {
theme = saved;
}
});
$effect(() => {
if (!browser) return;
const root = document.documentElement;
if (theme === 'dark') {
root.classList.add('dark');
} else if (theme === 'light') {
root.classList.remove('dark');
} else {
// System preference
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
root.classList.add('dark');
} else {
root.classList.remove('dark');
}
}
localStorage.setItem('theme', theme);
});
function cycle() {
const themes: Theme[] = ['light', 'dark', 'system'];
const current = themes.indexOf(theme);
theme = themes[(current + 1) % themes.length];
}
</script>
<button
onclick={cycle}
class="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800"
aria-label="Toggle theme"
>
{#if theme === 'light'}
☀️
{:else if theme === 'dark'}
🌙
{:else}
💻
{/if}
</button>
```
### Prevent Flash of Wrong Theme
```html
<!-- app.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<script>
// Prevent flash of wrong theme
(function() {
const theme = localStorage.getItem('theme');
if (theme === 'dark' ||
(!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
}
})();
</script>
%sveltekit.head%
</head>
<body>
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
```
---
## 📐 Responsive Design
### Tailwind Breakpoints
```svelte
<div class="
grid grid-cols-1
sm:grid-cols-2
md:grid-cols-3
lg:grid-cols-4
xl:grid-cols-5
gap-4
">
{#each items as item}
<div class="p-4 bg-white rounded-lg shadow">
{item.name}
</div>
{/each}
</div>
<!-- Responsive typography -->
<h1 class="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold">
Responsive Heading
</h1>
<!-- Hide/show based on screen size -->
<nav class="hidden md:flex gap-4">
<!-- Desktop navigation -->
</nav>
<button class="md:hidden">
<!-- Mobile menu button -->
</button>
```
### Container Queries (Tailwind v3.4+)
```svelte
<div class="@container">
<div class="@md:flex @md:gap-4">
<img class="@md:w-48" src="/image.jpg" alt="" />
<div>
<h2 class="@md:text-xl">Title</h2>
<p class="@md:text-base text-sm">Description</p>
</div>
</div>
</div>
```
---
## ✨ Animations
### Tailwind Animations
```svelte
<!-- Built-in animations -->
<div class="animate-spin">Spinning</div>
<div class="animate-pulse">Pulsing</div>
<div class="animate-bounce">Bouncing</div>
<!-- Custom animation in tailwind.config.js -->
<div class="animate-fade-in">Fading in</div>
```
```javascript
// tailwind.config.js
export default {
theme: {
extend: {
animation: {
'fade-in': 'fadeIn 0.3s ease-in-out',
'slide-up': 'slideUp 0.3s ease-out',
'slide-down': 'slideDown 0.3s ease-out',
},
keyframes: {
fadeIn: {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
slideUp: {
'0%': { transform: 'translateY(10px)', opacity: '0' },
'100%': { transform: 'translateY(0)', opacity: '1' },
},
slideDown: {
'0%': { transform: 'translateY(-10px)', opacity: '0' },
'100%': { transform: 'translateY(0)', opacity: '1' },
},
},
},
},
};
```
### Svelte Transitions
```svelte
<script lang="ts">
import { fade, fly, slide, scale, blur } from 'svelte/transition';
import { quintOut } from 'svelte/easing';
let visible = $state(true);
</script>
{#if visible}
<!-- Fade -->
<div transition:fade={{ duration: 300 }}>
Fading content
</div>
<!-- Fly -->
<div transition:fly={{ y: 20, duration: 300 }}>
Flying content
</div>
<!-- Slide -->
<div transition:slide={{ duration: 300 }}>
Sliding content
</div>
<!-- Scale -->
<div transition:scale={{ start: 0.95, duration: 300 }}>
Scaling content
</div>
<!-- Combined with easing -->
<div transition:fly={{ y: 20, duration: 400, easing: quintOut }}>
Smooth flying content
</div>
{/if}
<!-- In/out separately -->
{#if visible}
<div
in:fly={{ y: 20, duration: 300 }}
out:fade={{ duration: 200 }}
>
Different in/out
</div>
{/if}
```
### Custom Transition
```typescript
// src/lib/transitions/index.ts
import { cubicOut } from 'svelte/easing';
export function expandHeight(
node: Element,
{ duration = 300, easing = cubicOut } = {}
) {
const style = getComputedStyle(node);
const height = parseFloat(style.height);
const paddingTop = parseFloat(style.paddingTop);
const paddingBottom = parseFloat(style.paddingBottom);
return {
duration,
easing,
css: (t: number) => `
height: ${t * height}px;
padding-top: ${t * paddingTop}px;
padding-bottom: ${t * paddingBottom}px;
overflow: hidden;
`
};
}
```
---
## 📋 Best Practices
### ✅ Do:
- Use Tailwind for utility-first styling
- Create reusable component classes with `@layer components`
- Use CSS variables for design tokens
- Prefer `class:` directive for conditional classes
- Use `cn()` utility for merging classes
- Keep animations subtle and purposeful
### ❌ Don't:
- Mix inline styles with Tailwind randomly
- Override Tailwind classes with `!important`
- Create deeply nested CSS selectors
- Use `@apply` for everything (defeats the purpose)
- Forget dark mode variants
- Ignore accessibility (focus states, contrast)
+730
View File
@@ -0,0 +1,730 @@
# Svelte Accessibility (A11y) Guide
## Overview
Accessibility is crucial for inclusive web applications. Svelte provides built-in accessibility warnings, and this guide covers best practices for building accessible Svelte applications.
---
## 🎯 Core Principles (POUR)
1. **Perceivable** - Users can perceive the content
2. **Operable** - Users can operate the interface
3. **Understandable** - Users can understand the content
4. **Robust** - Content works across assistive technologies
---
## ⌨️ Keyboard Navigation
### Focus Management
```svelte
<script lang="ts">
import { tick } from 'svelte';
let dialogOpen = $state(false);
let triggerButton: HTMLButtonElement;
let dialogContent: HTMLDivElement;
async function openDialog() {
dialogOpen = true;
await tick();
// Move focus to dialog
dialogContent?.focus();
}
function closeDialog() {
dialogOpen = false;
// Return focus to trigger
triggerButton?.focus();
}
</script>
<button bind:this={triggerButton} onclick={openDialog}>
Open Dialog
</button>
{#if dialogOpen}
<div
bind:this={dialogContent}
role="dialog"
aria-modal="true"
aria-labelledby="dialog-title"
tabindex="-1"
>
<h2 id="dialog-title">Dialog Title</h2>
<p>Dialog content...</p>
<button onclick={closeDialog}>Close</button>
</div>
{/if}
```
### Focus Trap
```svelte
<script lang="ts">
let container: HTMLElement;
function trapFocus(e: KeyboardEvent) {
if (e.key !== 'Tab') return;
const focusable = container.querySelectorAll<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
</script>
<div bind:this={container} onkeydown={trapFocus}>
<!-- Focusable content -->
</div>
```
### Skip Links
```svelte
<!-- +layout.svelte -->
<a href="#main-content" class="skip-link">
Skip to main content
</a>
<nav>
<!-- Navigation -->
</nav>
<main id="main-content" tabindex="-1">
<slot />
</main>
<style>
.skip-link {
position: absolute;
top: -40px;
left: 0;
padding: 8px;
background: var(--color-primary);
color: white;
z-index: 100;
}
.skip-link:focus {
top: 0;
}
</style>
```
---
## 🏷️ ARIA Attributes
### Roles
```svelte
<!-- Landmark roles -->
<header role="banner">...</header>
<nav role="navigation" aria-label="Main">...</nav>
<main role="main">...</main>
<aside role="complementary">...</aside>
<footer role="contentinfo">...</footer>
<!-- Widget roles -->
<div role="alert">Important message!</div>
<div role="status" aria-live="polite">Status update</div>
<div role="dialog" aria-modal="true">Dialog content</div>
<div role="tablist">...</div>
<div role="menu">...</div>
```
### States & Properties
```svelte
<script lang="ts">
let expanded = $state(false);
let selected = $state(false);
let loading = $state(false);
</script>
<!-- Expandable content -->
<button
aria-expanded={expanded}
aria-controls="panel-1"
onclick={() => expanded = !expanded}
>
Toggle Panel
</button>
<div id="panel-1" hidden={!expanded}>
Panel content
</div>
<!-- Selection -->
<div
role="option"
aria-selected={selected}
onclick={() => selected = !selected}
>
Option
</div>
<!-- Loading state -->
<button
aria-busy={loading}
disabled={loading}
>
{loading ? 'Loading...' : 'Submit'}
</button>
<!-- Described by -->
<input
type="email"
aria-describedby="email-hint email-error"
/>
<p id="email-hint">We'll never share your email</p>
<p id="email-error" role="alert">Invalid email format</p>
```
### Live Regions
```svelte
<script lang="ts">
let message = $state('');
let items = $state<string[]>([]);
function addItem(item: string) {
items.push(item);
message = `${item} added to cart`;
// Clear message after announcement
setTimeout(() => message = '', 1000);
}
</script>
<!-- Polite announcements (waits for user to finish) -->
<div aria-live="polite" aria-atomic="true" class="sr-only">
{message}
</div>
<!-- Assertive announcements (interrupts immediately) -->
<div aria-live="assertive" role="alert">
{#if error}
{error}
{/if}
</div>
<!-- Status updates -->
<div role="status" aria-live="polite">
{items.length} items in cart
</div>
```
---
## 📝 Forms
### Accessible Form Pattern
```svelte
<script lang="ts">
interface FormState {
name: string;
email: string;
}
interface FormErrors {
name?: string;
email?: string;
}
let form = $state<FormState>({ name: '', email: '' });
let errors = $state<FormErrors>({});
let submitted = $state(false);
function validate(): boolean {
errors = {};
if (!form.name.trim()) {
errors.name = 'Name is required';
}
if (!form.email.trim()) {
errors.email = 'Email is required';
} else if (!form.email.includes('@')) {
errors.email = 'Please enter a valid email';
}
return Object.keys(errors).length === 0;
}
function handleSubmit(e: SubmitEvent) {
e.preventDefault();
submitted = true;
if (validate()) {
// Submit form
}
}
</script>
<form onsubmit={handleSubmit} novalidate>
<div class="field">
<label for="name">
Name
<span aria-hidden="true">*</span>
</label>
<input
id="name"
type="text"
bind:value={form.name}
aria-required="true"
aria-invalid={submitted && !!errors.name}
aria-describedby={errors.name ? 'name-error' : undefined}
/>
{#if submitted && errors.name}
<p id="name-error" class="error" role="alert">
{errors.name}
</p>
{/if}
</div>
<div class="field">
<label for="email">
Email
<span aria-hidden="true">*</span>
</label>
<input
id="email"
type="email"
bind:value={form.email}
aria-required="true"
aria-invalid={submitted && !!errors.email}
aria-describedby="email-hint {errors.email ? 'email-error' : ''}"
/>
<p id="email-hint" class="hint">
We'll use this to send you updates
</p>
{#if submitted && errors.email}
<p id="email-error" class="error" role="alert">
{errors.email}
</p>
{/if}
</div>
<button type="submit">Submit</button>
</form>
```
### Field Group
```svelte
<fieldset>
<legend>Shipping Address</legend>
<div class="field">
<label for="street">Street</label>
<input id="street" type="text" />
</div>
<div class="field">
<label for="city">City</label>
<input id="city" type="text" />
</div>
</fieldset>
<!-- Radio group -->
<fieldset>
<legend>Preferred contact method</legend>
<div>
<input type="radio" id="contact-email" name="contact" value="email" />
<label for="contact-email">Email</label>
</div>
<div>
<input type="radio" id="contact-phone" name="contact" value="phone" />
<label for="contact-phone">Phone</label>
</div>
</fieldset>
```
---
## 🖼️ Images & Media
### Image Alternatives
```svelte
<!-- Informative image -->
<img src="/chart.png" alt="Sales increased 25% in Q3 2024" />
<!-- Decorative image -->
<img src="/decoration.svg" alt="" role="presentation" />
<!-- Complex image with long description -->
<figure>
<img
src="/complex-chart.png"
alt="Revenue comparison chart"
aria-describedby="chart-description"
/>
<figcaption id="chart-description">
This chart shows quarterly revenue for 2024. Q1: $1.2M, Q2: $1.5M,
Q3: $1.9M, Q4: $2.1M. Total annual revenue was $6.7M, representing
a 15% increase over 2023.
</figcaption>
</figure>
<!-- Icon with meaning -->
<button>
<svg aria-hidden="true"><!-- icon --></svg>
<span class="sr-only">Delete item</span>
</button>
<!-- Or -->
<button aria-label="Delete item">
<svg aria-hidden="true"><!-- icon --></svg>
</button>
```
### Video Accessibility
```svelte
<video controls>
<source src="/video.mp4" type="video/mp4" />
<track kind="captions" src="/captions.vtt" srclang="en" label="English" default />
<track kind="descriptions" src="/descriptions.vtt" srclang="en" label="Audio descriptions" />
<p>Your browser doesn't support HTML5 video. <a href="/video.mp4">Download the video</a>.</p>
</video>
```
---
## 🧩 Accessible Components
### Accordion
```svelte
<script lang="ts">
interface Item {
id: string;
title: string;
content: string;
}
let { items }: { items: Item[] } = $props();
let openId = $state<string | null>(null);
function toggle(id: string) {
openId = openId === id ? null : id;
}
function handleKeydown(e: KeyboardEvent, index: number) {
const buttons = document.querySelectorAll<HTMLButtonElement>('[data-accordion-trigger]');
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
buttons[(index + 1) % buttons.length]?.focus();
break;
case 'ArrowUp':
e.preventDefault();
buttons[(index - 1 + buttons.length) % buttons.length]?.focus();
break;
case 'Home':
e.preventDefault();
buttons[0]?.focus();
break;
case 'End':
e.preventDefault();
buttons[buttons.length - 1]?.focus();
break;
}
}
</script>
<div class="accordion">
{#each items as item, index (item.id)}
{@const isOpen = openId === item.id}
<h3>
<button
data-accordion-trigger
id="accordion-header-{item.id}"
aria-expanded={isOpen}
aria-controls="accordion-panel-{item.id}"
onclick={() => toggle(item.id)}
onkeydown={(e) => handleKeydown(e, index)}
>
{item.title}
<span aria-hidden="true">{isOpen ? '' : '+'}</span>
</button>
</h3>
<div
id="accordion-panel-{item.id}"
role="region"
aria-labelledby="accordion-header-{item.id}"
hidden={!isOpen}
>
<p>{item.content}</p>
</div>
{/each}
</div>
```
### Tabs
```svelte
<script lang="ts">
interface Tab {
id: string;
label: string;
content: string;
}
let { tabs }: { tabs: Tab[] } = $props();
let activeTab = $state(tabs[0]?.id);
function handleKeydown(e: KeyboardEvent) {
const currentIndex = tabs.findIndex(t => t.id === activeTab);
switch (e.key) {
case 'ArrowRight':
activeTab = tabs[(currentIndex + 1) % tabs.length].id;
break;
case 'ArrowLeft':
activeTab = tabs[(currentIndex - 1 + tabs.length) % tabs.length].id;
break;
case 'Home':
activeTab = tabs[0].id;
break;
case 'End':
activeTab = tabs[tabs.length - 1].id;
break;
}
}
</script>
<div class="tabs">
<div role="tablist" aria-label="Content tabs">
{#each tabs as tab (tab.id)}
{@const isActive = activeTab === tab.id}
<button
role="tab"
id="tab-{tab.id}"
aria-selected={isActive}
aria-controls="panel-{tab.id}"
tabindex={isActive ? 0 : -1}
onclick={() => activeTab = tab.id}
onkeydown={handleKeydown}
>
{tab.label}
</button>
{/each}
</div>
{#each tabs as tab (tab.id)}
<div
role="tabpanel"
id="panel-{tab.id}"
aria-labelledby="tab-{tab.id}"
hidden={activeTab !== tab.id}
tabindex="0"
>
{tab.content}
</div>
{/each}
</div>
```
### Modal Dialog
```svelte
<script lang="ts">
import type { Snippet } from 'svelte';
import { tick } from 'svelte';
interface Props {
open: boolean;
title: string;
onclose: () => void;
children: Snippet;
}
let { open, title, onclose, children }: Props = $props();
let dialog: HTMLDialogElement;
let previousFocus: HTMLElement | null = null;
$effect(() => {
if (open) {
previousFocus = document.activeElement as HTMLElement;
tick().then(() => {
dialog?.showModal();
});
} else {
dialog?.close();
previousFocus?.focus();
}
});
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') {
onclose();
}
}
function handleBackdrop(e: MouseEvent) {
if (e.target === dialog) {
onclose();
}
}
</script>
{#if open}
<dialog
bind:this={dialog}
onkeydown={handleKeydown}
onclick={handleBackdrop}
aria-labelledby="dialog-title"
aria-describedby="dialog-description"
aria-modal="true"
>
<div class="dialog-content">
<header>
<h2 id="dialog-title">{title}</h2>
<button
onclick={onclose}
aria-label="Close dialog"
>
×
</button>
</header>
<div id="dialog-description">
{@render children()}
</div>
</div>
</dialog>
{/if}
<style>
dialog::backdrop {
background: rgba(0, 0, 0, 0.5);
}
</style>
```
---
## 📱 Screen Reader Utilities
### Visually Hidden Content
```css
/* In app.css or component */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* Can be focused (for skip links) */
.sr-only-focusable:focus {
position: static;
width: auto;
height: auto;
padding: inherit;
margin: inherit;
overflow: visible;
clip: auto;
white-space: normal;
}
```
```svelte
<!-- Usage -->
<span class="sr-only">Total price:</span>
<span>$99.99</span>
<button>
<svg aria-hidden="true"><!-- icon --></svg>
<span class="sr-only">Add to cart</span>
</button>
```
---
## ✅ Accessibility Checklist
### Structure
- [ ] Proper heading hierarchy (h1 → h2 → h3)
- [ ] Landmark regions (header, nav, main, footer)
- [ ] Skip links for keyboard users
- [ ] Logical reading order
### Interactivity
- [ ] All functionality keyboard accessible
- [ ] Visible focus indicators
- [ ] Focus management for modals/dialogs
- [ ] No keyboard traps
### Forms
- [ ] Labels associated with inputs
- [ ] Error messages linked with aria-describedby
- [ ] Required fields indicated
- [ ] Validation errors announced
### Images & Media
- [ ] Meaningful alt text
- [ ] Decorative images have empty alt
- [ ] Video captions/transcripts
- [ ] Audio descriptions where needed
### Color & Contrast
- [ ] 4.5:1 contrast for normal text
- [ ] 3:1 contrast for large text
- [ ] Information not conveyed by color alone
- [ ] Focus indicators visible
### Dynamic Content
- [ ] Live regions for updates
- [ ] Loading states announced
- [ ] Error states announced
- [ ] Changes in context warned
---
## 🧪 Testing Tools
```bash
# Install axe-core for automated testing
npm install -D @axe-core/playwright
# Or use browser extensions:
# - axe DevTools
# - WAVE Evaluation Tool
# - Accessibility Insights
```
### Manual Testing Checklist
1. Tab through entire page
2. Use screen reader (VoiceOver, NVDA, JAWS)
3. Zoom to 200%
4. Disable CSS
5. Check in high contrast mode
+651
View File
@@ -0,0 +1,651 @@
# Svelte Performance Optimization Guide
## Overview
Svelte is already highly performant by design, but there are patterns to ensure your applications stay fast as they grow.
---
## 📊 Understanding Svelte Reactivity
### How Svelte 5 Reactivity Works
```svelte
<script lang="ts">
// $state creates a reactive proxy
let items = $state<string[]>([]);
// Svelte tracks which properties are READ in which contexts
// Only those contexts re-run when properties change
// This creates a dependency on items.length
let count = $derived(items.length);
// This effect only runs when items changes
$effect(() => {
console.log('Items changed:', items);
});
</script>
```
### Avoid Unnecessary Reactivity
```svelte
<script lang="ts">
// ❌ BAD: Everything is reactive
let config = $state({
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3
});
// ✅ GOOD: Static config doesn't need reactivity
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3
};
// ✅ GOOD: Use $state.raw for data that's replaced, not mutated
let largeDataset = $state.raw<Record[]>([]);
async function fetchData() {
// Replace entirely instead of mutating
largeDataset = await fetch('/api/data').then(r => r.json());
}
</script>
```
---
## 🚀 Component Optimization
### 1. Minimize Re-renders
```svelte
<script lang="ts">
let items = $state<Item[]>([]);
let filter = $state('');
// ✅ GOOD: Computed once, cached until dependencies change
let filteredItems = $derived(
items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
)
);
// ❌ BAD: Recomputed on every render
// Using inline: items.filter(...) in template
</script>
<!-- ✅ GOOD: Uses cached derived value -->
{#each filteredItems as item (item.id)}
<ItemCard {item} />
{/each}
```
### 2. Key Your Each Blocks
```svelte
<!-- ❌ BAD: No key, Svelte can't optimize updates -->
{#each items as item}
<Item {item} />
{/each}
<!-- ✅ GOOD: Unique key enables efficient diffing -->
{#each items as item (item.id)}
<Item {item} />
{/each}
<!-- ✅ GOOD: Index as key only for static lists -->
{#each staticLabels as label, i (i)}
<span>{label}</span>
{/each}
```
### 3. Lazy Load Components
```svelte
<script lang="ts">
// Lazy load heavy components
let showChart = $state(false);
// Dynamic import
async function loadChart() {
const { Chart } = await import('./Chart.svelte');
return Chart;
}
</script>
{#if showChart}
{#await loadChart() then Chart}
<Chart data={chartData} />
{/await}
{/if}
```
### 4. Virtualization for Long Lists
```svelte
<!-- Use a virtualization library for long lists -->
<script lang="ts">
import VirtualList from '$lib/components/VirtualList.svelte';
let items = $state<Item[]>([]);
// Could be 10,000+ items
</script>
<!-- Only renders visible items -->
<VirtualList
{items}
itemHeight={50}
let:item
>
<ItemRow {item} />
</VirtualList>
```
Simple Virtual List Implementation:
```svelte
<!-- VirtualList.svelte -->
<script lang="ts" generics="T">
import type { Snippet } from 'svelte';
interface Props {
items: T[];
itemHeight: number;
containerHeight?: number;
overscan?: number;
children: Snippet<[T, number]>;
}
let {
items,
itemHeight,
containerHeight = 400,
overscan = 3,
children
}: Props = $props();
let scrollTop = $state(0);
let totalHeight = $derived(items.length * itemHeight);
let startIndex = $derived(
Math.max(0, Math.floor(scrollTop / itemHeight) - overscan)
);
let endIndex = $derived(
Math.min(
items.length,
Math.ceil((scrollTop + containerHeight) / itemHeight) + overscan
)
);
let visibleItems = $derived(
items.slice(startIndex, endIndex).map((item, i) => ({
item,
index: startIndex + i
}))
);
let offsetY = $derived(startIndex * itemHeight);
</script>
<div
class="virtual-list-container"
style:height="{containerHeight}px"
onscroll={(e) => scrollTop = e.currentTarget.scrollTop}
>
<div
class="virtual-list-content"
style:height="{totalHeight}px"
>
<div style:transform="translateY({offsetY}px)">
{#each visibleItems as { item, index } (index)}
<div style:height="{itemHeight}px">
{@render children(item, index)}
</div>
{/each}
</div>
</div>
</div>
<style>
.virtual-list-container {
overflow-y: auto;
}
.virtual-list-content {
position: relative;
}
</style>
```
---
## ⚡ Data Loading Optimization
### 1. Parallel Loading
```typescript
// src/routes/dashboard/+page.ts
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ fetch }) => {
// ✅ GOOD: Parallel fetching
const [users, posts, stats] = await Promise.all([
fetch('/api/users').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/stats').then(r => r.json())
]);
return { users, posts, stats };
};
// ❌ BAD: Sequential fetching
export const loadBad: PageLoad = async ({ fetch }) => {
const users = await fetch('/api/users').then(r => r.json());
const posts = await fetch('/api/posts').then(r => r.json());
const stats = await fetch('/api/stats').then(r => r.json());
return { users, posts, stats };
};
```
### 2. Streaming with Deferred Loading
```typescript
// src/routes/dashboard/+page.server.ts
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ fetch }) => {
// Critical data loads immediately
const user = await fetch('/api/user').then(r => r.json());
// Non-critical data streams in
return {
user,
// These are promises, will stream when ready
posts: fetch('/api/posts').then(r => r.json()),
recommendations: fetch('/api/recommendations').then(r => r.json())
};
};
```
```svelte
<!-- +page.svelte -->
<script lang="ts">
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
</script>
<!-- Immediate content -->
<h1>Welcome, {data.user.name}</h1>
<!-- Streamed content -->
{#await data.posts}
<PostsSkeleton />
{:then posts}
<PostsList {posts} />
{/await}
{#await data.recommendations}
<RecommendationsSkeleton />
{:then recommendations}
<Recommendations {recommendations} />
{/await}
```
### 3. Prefetching
```svelte
<!-- SvelteKit auto-prefetches on hover -->
<a href="/about">About</a>
<!-- Prefetch on viewport entry -->
<a href="/products" data-sveltekit-preload-data="hover">Products</a>
<!-- Programmatic prefetch -->
<script lang="ts">
import { preloadData } from '$app/navigation';
// Prefetch when likely needed
function handleMouseEnter() {
preloadData('/expensive-page');
}
</script>
```
---
## 🖼️ Image Optimization
### Responsive Images
```svelte
<picture>
<source
media="(min-width: 1024px)"
srcset="/hero-large.webp 1x, /hero-large@2x.webp 2x"
type="image/webp"
/>
<source
media="(min-width: 640px)"
srcset="/hero-medium.webp 1x, /hero-medium@2x.webp 2x"
type="image/webp"
/>
<img
src="/hero-small.jpg"
srcset="/hero-small.jpg 1x, /hero-small@2x.jpg 2x"
alt="Hero image"
loading="lazy"
decoding="async"
width="800"
height="400"
/>
</picture>
```
### Lazy Loading Images
```svelte
<script lang="ts">
interface Props {
src: string;
alt: string;
width: number;
height: number;
}
let { src, alt, width, height }: Props = $props();
let loaded = $state(false);
let element: HTMLImageElement;
$effect(() => {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
element.src = src;
observer.disconnect();
}
},
{ rootMargin: '200px' }
);
observer.observe(element);
return () => observer.disconnect();
});
</script>
<div class="image-wrapper" style:aspect-ratio="{width}/{height}">
<img
bind:this={element}
{alt}
{width}
{height}
class:loaded
onload={() => loaded = true}
/>
{#if !loaded}
<div class="placeholder" />
{/if}
</div>
<style>
.image-wrapper {
position: relative;
overflow: hidden;
background: #f0f0f0;
}
img {
opacity: 0;
transition: opacity 0.3s;
}
img.loaded {
opacity: 1;
}
.placeholder {
position: absolute;
inset: 0;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
</style>
```
---
## 🔄 Effect Optimization
### Debounce Effects
```svelte
<script lang="ts">
let searchQuery = $state('');
let results = $state<SearchResult[]>([]);
// ❌ BAD: Fires on every keystroke
$effect(() => {
fetchResults(searchQuery);
});
// ✅ GOOD: Debounced
$effect(() => {
const query = searchQuery; // Read reactive value
const timeout = setTimeout(() => {
if (query) {
fetchResults(query).then(r => results = r);
}
}, 300);
return () => clearTimeout(timeout);
});
</script>
```
### Avoid Heavy Computations in Effects
```svelte
<script lang="ts">
let items = $state<Item[]>([]);
// ❌ BAD: Heavy computation in effect
$effect(() => {
const sorted = [...items].sort((a, b) => {
// Complex sorting logic
});
// Do something with sorted
});
// ✅ GOOD: Use $derived for computations
let sortedItems = $derived.by(() => {
return [...items].sort((a, b) => {
// Complex sorting logic
});
});
// Effect only for side effects
$effect(() => {
console.log('Sorted items updated:', sortedItems.length);
});
</script>
```
### Cleanup Long-Running Effects
```svelte
<script lang="ts">
let data = $state<Data | null>(null);
$effect(() => {
const controller = new AbortController();
fetch('/api/data', { signal: controller.signal })
.then(r => r.json())
.then(d => data = d)
.catch(e => {
if (e.name !== 'AbortError') {
console.error(e);
}
});
// Cleanup: abort if component unmounts or effect re-runs
return () => controller.abort();
});
</script>
```
---
## 📦 Bundle Optimization
### Code Splitting
```typescript
// vite.config.ts
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()],
build: {
rollupOptions: {
output: {
manualChunks: {
// Vendor chunks
'vendor-svelte': ['svelte'],
'vendor-chart': ['chart.js'],
}
}
}
}
});
```
### Tree Shaking
```typescript
// ✅ GOOD: Named imports enable tree shaking
import { formatDate, formatCurrency } from '$lib/utils';
// ❌ BAD: Imports entire module
import * as utils from '$lib/utils';
```
### Analyze Bundle
```bash
# Install analyzer
npm install -D rollup-plugin-visualizer
# Add to vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({
plugins: [
sveltekit(),
visualizer({
filename: 'stats.html',
open: true
})
]
});
```
---
## 📏 Measuring Performance
### Browser DevTools
```svelte
<script lang="ts">
import { onMount } from 'svelte';
onMount(() => {
// Measure component mount time
performance.mark('component-start');
return () => {
performance.mark('component-end');
performance.measure('component-lifecycle', 'component-start', 'component-end');
};
});
</script>
```
### Core Web Vitals
```typescript
// src/lib/vitals.ts
import type { Metric } from 'web-vitals';
export function reportVitals(metric: Metric) {
// Send to analytics
console.log(metric.name, metric.value);
// Or send to your analytics service
fetch('/api/vitals', {
method: 'POST',
body: JSON.stringify(metric)
});
}
// In +layout.svelte
import { onMount } from 'svelte';
import { reportVitals } from '$lib/vitals';
onMount(async () => {
const { onCLS, onFID, onFCP, onLCP, onTTFB } = await import('web-vitals');
onCLS(reportVitals);
onFID(reportVitals);
onFCP(reportVitals);
onLCP(reportVitals);
onTTFB(reportVitals);
});
```
---
## 📋 Performance Checklist
### Build Time
- [ ] Minimal dependencies
- [ ] Tree-shakeable imports
- [ ] Code splitting configured
- [ ] Image optimization
### Runtime
- [ ] Keys on `{#each}` blocks
- [ ] Virtualization for long lists
- [ ] Debounced inputs
- [ ] Lazy loaded heavy components
- [ ] Optimized images (lazy, responsive)
### Data Loading
- [ ] Parallel fetching where possible
- [ ] Streaming for non-critical data
- [ ] Prefetching for navigation
- [ ] Caching strategies
### Effects
- [ ] Cleanup functions for subscriptions
- [ ] Debounced network requests
- [ ] Heavy computations in `$derived`
- [ ] AbortController for fetch
+695
View File
@@ -0,0 +1,695 @@
# Svelte Testing Guide
## Overview
Testing Svelte applications involves unit testing components, integration testing with stores/context, and end-to-end testing with real browsers.
---
## 🔧 Testing Setup
### Install Dependencies
```bash
# Vitest for unit/integration tests
npm install -D vitest @testing-library/svelte @testing-library/jest-dom jsdom
# Playwright for E2E tests
npm install -D @playwright/test
npx playwright install
```
### vitest.config.ts
```typescript
import { defineConfig } from 'vitest/config';
import { svelte } from '@sveltejs/vite-plugin-svelte';
export default defineConfig({
plugins: [svelte({ hot: !process.env.VITEST })],
test: {
include: ['src/**/*.{test,spec}.{js,ts}'],
globals: true,
environment: 'jsdom',
setupFiles: ['./src/tests/setup.ts'],
coverage: {
reporter: ['text', 'json', 'html'],
exclude: ['node_modules/', 'src/tests/']
}
}
});
```
### Test Setup File
```typescript
// src/tests/setup.ts
import '@testing-library/jest-dom/vitest';
import { vi } from 'vitest';
// Mock SvelteKit modules
vi.mock('$app/navigation', () => ({
goto: vi.fn(),
invalidate: vi.fn(),
invalidateAll: vi.fn(),
preloadData: vi.fn(),
preloadCode: vi.fn()
}));
vi.mock('$app/stores', () => ({
page: {
subscribe: vi.fn()
},
navigating: {
subscribe: vi.fn()
}
}));
vi.mock('$app/environment', () => ({
browser: true,
dev: true,
building: false
}));
```
---
## 🧪 Unit Testing Components
### Basic Component Test
```typescript
// src/lib/components/Button.test.ts
import { render, screen, fireEvent } from '@testing-library/svelte';
import { describe, it, expect, vi } from 'vitest';
import Button from './Button.svelte';
describe('Button', () => {
it('renders with text', () => {
render(Button, { props: { children: 'Click me' } });
expect(screen.getByRole('button')).toHaveTextContent('Click me');
});
it('applies variant class', () => {
render(Button, {
props: {
variant: 'primary',
children: 'Primary'
}
});
expect(screen.getByRole('button')).toHaveClass('btn-primary');
});
it('handles click events', async () => {
const onclick = vi.fn();
render(Button, {
props: {
onclick,
children: 'Click'
}
});
await fireEvent.click(screen.getByRole('button'));
expect(onclick).toHaveBeenCalledOnce();
});
it('is disabled when loading', () => {
render(Button, {
props: {
loading: true,
children: 'Submit'
}
});
expect(screen.getByRole('button')).toBeDisabled();
});
});
```
### Testing with Snippets (Svelte 5)
```typescript
// src/lib/components/Card.test.ts
import { render, screen } from '@testing-library/svelte';
import { describe, it, expect } from 'vitest';
import Card from './Card.svelte';
// Helper to create snippet-like content
const createCardWithContent = (title: string, content: string) => {
// For Svelte 5, you may need a wrapper component
return {
component: Card,
props: {
title
},
// Slot content for testing
slots: {
default: content
}
};
};
describe('Card', () => {
it('renders title and content', () => {
render(Card, {
props: { title: 'Card Title' }
// Note: Testing snippets may require wrapper components
});
expect(screen.getByText('Card Title')).toBeInTheDocument();
});
});
```
### Testing Reactive State
```typescript
// src/lib/components/Counter.test.ts
import { render, screen, fireEvent } from '@testing-library/svelte';
import { describe, it, expect } from 'vitest';
import Counter from './Counter.svelte';
describe('Counter', () => {
it('increments count on click', async () => {
render(Counter);
const button = screen.getByRole('button', { name: /increment/i });
const display = screen.getByTestId('count');
expect(display).toHaveTextContent('0');
await fireEvent.click(button);
expect(display).toHaveTextContent('1');
await fireEvent.click(button);
await fireEvent.click(button);
expect(display).toHaveTextContent('3');
});
it('starts with initial value', () => {
render(Counter, { props: { initialValue: 10 } });
expect(screen.getByTestId('count')).toHaveTextContent('10');
});
});
```
### Testing Async Components
```typescript
// src/lib/components/UserProfile.test.ts
import { render, screen, waitFor } from '@testing-library/svelte';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import UserProfile from './UserProfile.svelte';
// Mock fetch
const mockFetch = vi.fn();
global.fetch = mockFetch;
describe('UserProfile', () => {
beforeEach(() => {
mockFetch.mockReset();
});
it('shows loading state', () => {
mockFetch.mockImplementation(() => new Promise(() => {})); // Never resolves
render(UserProfile, { props: { userId: '1' } });
expect(screen.getByText('Loading...')).toBeInTheDocument();
});
it('displays user data after loading', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ name: 'John Doe', email: 'john@example.com' })
});
render(UserProfile, { props: { userId: '1' } });
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument();
});
expect(screen.getByText('john@example.com')).toBeInTheDocument();
});
it('shows error on fetch failure', async () => {
mockFetch.mockRejectedValueOnce(new Error('Network error'));
render(UserProfile, { props: { userId: '1' } });
await waitFor(() => {
expect(screen.getByRole('alert')).toHaveTextContent('Failed to load user');
});
});
});
```
---
## 🏪 Testing Stores
### Testing .svelte.ts Stores
```typescript
// src/lib/stores/counter.svelte.ts
export class Counter {
count = $state(0);
doubled = $derived(this.count * 2);
increment() {
this.count++;
}
decrement() {
this.count--;
}
reset() {
this.count = 0;
}
}
// src/lib/stores/counter.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { Counter } from './counter.svelte';
describe('Counter Store', () => {
let counter: Counter;
beforeEach(() => {
counter = new Counter();
});
it('starts at 0', () => {
expect(counter.count).toBe(0);
});
it('increments', () => {
counter.increment();
expect(counter.count).toBe(1);
counter.increment();
counter.increment();
expect(counter.count).toBe(3);
});
it('decrements', () => {
counter.count = 5;
counter.decrement();
expect(counter.count).toBe(4);
});
it('computes doubled value', () => {
counter.count = 5;
expect(counter.doubled).toBe(10);
counter.increment();
expect(counter.doubled).toBe(12);
});
it('resets to 0', () => {
counter.count = 100;
counter.reset();
expect(counter.count).toBe(0);
});
});
```
### Testing Async Stores
```typescript
// src/lib/stores/users.svelte.ts
export class UsersStore {
users = $state<User[]>([]);
loading = $state(false);
error = $state<string | null>(null);
async fetchUsers() {
this.loading = true;
this.error = null;
try {
const response = await fetch('/api/users');
if (!response.ok) throw new Error('Failed to fetch');
this.users = await response.json();
} catch (e) {
this.error = e instanceof Error ? e.message : 'Unknown error';
} finally {
this.loading = false;
}
}
}
// src/lib/stores/users.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { UsersStore } from './users.svelte';
const mockFetch = vi.fn();
global.fetch = mockFetch;
describe('UsersStore', () => {
let store: UsersStore;
beforeEach(() => {
store = new UsersStore();
mockFetch.mockReset();
});
it('fetches users successfully', async () => {
const mockUsers = [
{ id: '1', name: 'John' },
{ id: '2', name: 'Jane' }
];
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockUsers)
});
await store.fetchUsers();
expect(store.users).toEqual(mockUsers);
expect(store.loading).toBe(false);
expect(store.error).toBeNull();
});
it('handles fetch error', async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
status: 500
});
await store.fetchUsers();
expect(store.users).toEqual([]);
expect(store.error).toBe('Failed to fetch');
});
it('sets loading state during fetch', async () => {
mockFetch.mockImplementation(() => new Promise(resolve => {
setTimeout(() => resolve({
ok: true,
json: () => Promise.resolve([])
}), 100);
}));
const promise = store.fetchUsers();
expect(store.loading).toBe(true);
await promise;
expect(store.loading).toBe(false);
});
});
```
---
## 🔗 Integration Testing
### Testing Components with Context
```typescript
// src/lib/components/ThemeConsumer.test.ts
import { render, screen } from '@testing-library/svelte';
import { describe, it, expect } from 'vitest';
import ThemeProvider from './ThemeProvider.svelte';
import ThemeConsumer from './ThemeConsumer.svelte';
// Wrapper component for testing
import TestWrapper from './TestWrapper.svelte';
describe('ThemeConsumer', () => {
it('uses theme from provider', () => {
// TestWrapper.svelte wraps children in ThemeProvider
render(TestWrapper, {
props: {
component: ThemeConsumer,
theme: 'dark'
}
});
expect(screen.getByTestId('theme-display')).toHaveTextContent('dark');
});
});
```
### Testing Form Submissions
```typescript
// src/lib/components/LoginForm.test.ts
import { render, screen, fireEvent, waitFor } from '@testing-library/svelte';
import { describe, it, expect, vi } from 'vitest';
import LoginForm from './LoginForm.svelte';
describe('LoginForm', () => {
it('submits form with credentials', async () => {
const onsubmit = vi.fn();
render(LoginForm, { props: { onsubmit } });
await fireEvent.input(
screen.getByLabelText('Email'),
{ target: { value: 'test@example.com' } }
);
await fireEvent.input(
screen.getByLabelText('Password'),
{ target: { value: 'password123' } }
);
await fireEvent.click(screen.getByRole('button', { name: /login/i }));
expect(onsubmit).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password123'
});
});
it('shows validation errors', async () => {
render(LoginForm);
await fireEvent.click(screen.getByRole('button', { name: /login/i }));
expect(screen.getByText('Email is required')).toBeInTheDocument();
expect(screen.getByText('Password is required')).toBeInTheDocument();
});
it('disables button while loading', async () => {
render(LoginForm, { props: { loading: true } });
expect(screen.getByRole('button')).toBeDisabled();
});
});
```
---
## 🌐 E2E Testing with Playwright
### playwright.config.ts
```typescript
import type { PlaywrightTestConfig } from '@playwright/test';
const config: PlaywrightTestConfig = {
webServer: {
command: 'npm run build && npm run preview',
port: 4173
},
testDir: 'tests',
testMatch: /(.+\.)?(test|spec)\.[jt]s/,
use: {
baseURL: 'http://localhost:4173',
trace: 'on-first-retry',
screenshot: 'only-on-failure'
},
projects: [
{
name: 'chromium',
use: { browserName: 'chromium' }
},
{
name: 'firefox',
use: { browserName: 'firefox' }
},
{
name: 'webkit',
use: { browserName: 'webkit' }
}
]
};
export default config;
```
### E2E Test Examples
```typescript
// tests/auth.test.ts
import { test, expect } from '@playwright/test';
test.describe('Authentication', () => {
test('user can log in', async ({ page }) => {
await page.goto('/login');
await page.fill('input[name="email"]', 'test@example.com');
await page.fill('input[name="password"]', 'password123');
await page.click('button[type="submit"]');
// Should redirect to dashboard
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('h1')).toContainText('Dashboard');
});
test('shows error for invalid credentials', async ({ page }) => {
await page.goto('/login');
await page.fill('input[name="email"]', 'wrong@example.com');
await page.fill('input[name="password"]', 'wrongpassword');
await page.click('button[type="submit"]');
await expect(page.locator('[role="alert"]')).toContainText('Invalid credentials');
await expect(page).toHaveURL('/login');
});
test('protected routes redirect to login', async ({ page }) => {
await page.goto('/dashboard');
await expect(page).toHaveURL('/login');
});
});
// tests/navigation.test.ts
test.describe('Navigation', () => {
test('main navigation works', async ({ page }) => {
await page.goto('/');
await page.click('a[href="/about"]');
await expect(page).toHaveURL('/about');
await expect(page.locator('h1')).toContainText('About');
await page.click('a[href="/contact"]');
await expect(page).toHaveURL('/contact');
await expect(page.locator('h1')).toContainText('Contact');
});
test('back button works', async ({ page }) => {
await page.goto('/');
await page.click('a[href="/about"]');
await page.goBack();
await expect(page).toHaveURL('/');
});
});
// tests/forms.test.ts
test.describe('Contact Form', () => {
test('submits form successfully', async ({ page }) => {
await page.goto('/contact');
await page.fill('input[name="name"]', 'John Doe');
await page.fill('input[name="email"]', 'john@example.com');
await page.fill('textarea[name="message"]', 'Hello, this is a test message');
await page.click('button[type="submit"]');
await expect(page.locator('.success-message')).toBeVisible();
await expect(page.locator('.success-message')).toContainText('Message sent');
});
});
```
### Testing Accessibility with Playwright
```typescript
// tests/accessibility.test.ts
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test.describe('Accessibility', () => {
test('home page passes axe', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});
test('login page passes axe', async ({ page }) => {
await page.goto('/login');
const results = await new AxeBuilder({ page })
.exclude('.third-party-widget') // Exclude elements you can't control
.analyze();
expect(results.violations).toEqual([]);
});
test('keyboard navigation works', async ({ page }) => {
await page.goto('/');
// Tab through focusable elements
await page.keyboard.press('Tab');
await expect(page.locator(':focus')).toHaveAttribute('href', '/');
await page.keyboard.press('Tab');
await expect(page.locator(':focus')).toHaveText('About');
// Can activate with Enter
await page.keyboard.press('Enter');
await expect(page).toHaveURL('/about');
});
});
```
---
## 📋 Package.json Scripts
```json
{
"scripts": {
"test": "vitest",
"test:unit": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"test:all": "npm run test:unit && npm run test:e2e"
}
}
```
---
## 📋 Testing Checklist
### Unit Tests
- [ ] All components have tests
- [ ] Props and variants tested
- [ ] Event handlers tested
- [ ] Edge cases covered
- [ ] Async behavior tested
### Integration Tests
- [ ] Store interactions tested
- [ ] Context providers tested
- [ ] Form submissions tested
- [ ] API calls mocked
### E2E Tests
- [ ] Critical user flows covered
- [ ] Authentication tested
- [ ] Form submissions tested
- [ ] Error states tested
- [ ] Accessibility automated
+656
View File
@@ -0,0 +1,656 @@
# SvelteKit Deployment Guide
## Overview
SvelteKit supports multiple deployment targets through adapters. Choose the right adapter for your infrastructure and requirements.
---
## 🔌 Available Adapters
### Adapter Comparison
| Adapter | Platform | SSR | SSG | Edge | Serverless |
|---------|----------|-----|-----|------|------------|
| `adapter-node` | Node.js | ✅ | ✅ | ❌ | ❌ |
| `adapter-auto` | Auto-detect | ✅ | ✅ | Varies | Varies |
| `adapter-vercel` | Vercel | ✅ | ✅ | ✅ | ✅ |
| `adapter-netlify` | Netlify | ✅ | ✅ | ✅ | ✅ |
| `adapter-cloudflare` | Cloudflare | ✅ | ✅ | ✅ | ✅ |
| `adapter-static` | Static hosts | ❌ | ✅ | ❌ | ❌ |
---
## 🖥️ Node.js Deployment (adapter-node)
### Installation
```bash
npm install -D @sveltejs/adapter-node
```
### Configuration
```javascript
// svelte.config.js
import adapter from '@sveltejs/adapter-node';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
preprocess: vitePreprocess(),
kit: {
adapter: adapter({
// Output directory
out: 'build',
// Precompress static assets with gzip/brotli
precompress: true,
// Environment variable configuration
envPrefix: 'APP_'
})
}
};
export default config;
```
### Build & Run
```bash
# Build
npm run build
# Run with Node
node build/index.js
# Or with environment variables
PORT=3000 HOST=0.0.0.0 node build/index.js
```
### Environment Variables
```bash
# Server configuration
PORT=3000 # Default: 3000
HOST=0.0.0.0 # Default: 0.0.0.0
ORIGIN=https://example.com # Required for form actions
BODY_SIZE_LIMIT=512K # Request body limit
```
### Production Process Manager (PM2)
```javascript
// ecosystem.config.cjs
module.exports = {
apps: [{
name: 'sveltekit-app',
script: './build/index.js',
instances: 'max',
exec_mode: 'cluster',
env: {
NODE_ENV: 'production',
PORT: 3000,
ORIGIN: 'https://example.com'
},
error_file: './logs/error.log',
out_file: './logs/output.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss Z'
}]
};
```
```bash
# Start with PM2
pm2 start ecosystem.config.cjs
# Save process list
pm2 save
# Setup startup script
pm2 startup
```
### Systemd Service
```ini
# /etc/systemd/system/sveltekit-app.service
[Unit]
Description=SvelteKit Application
After=network.target
[Service]
Type=simple
User=www-data
WorkingDirectory=/var/www/app
ExecStart=/usr/bin/node build/index.js
Restart=always
RestartSec=10
Environment=NODE_ENV=production
Environment=PORT=3000
Environment=ORIGIN=https://example.com
[Install]
WantedBy=multi-user.target
```
```bash
sudo systemctl enable sveltekit-app
sudo systemctl start sveltekit-app
```
### Nginx Reverse Proxy
```nginx
# /etc/nginx/sites-available/sveltekit-app
upstream sveltekit {
server 127.0.0.1:3000;
keepalive 64;
}
server {
listen 80;
server_name example.com;
# Redirect to HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com;
# SSL certificates
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# SSL settings
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers off;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# Gzip
gzip on;
gzip_types text/plain text/css application/json application/javascript;
# Static files with immutable cache
location /_app/immutable {
proxy_pass http://sveltekit;
add_header Cache-Control "public, max-age=31536000, immutable";
}
# Other static files
location /static {
proxy_pass http://sveltekit;
add_header Cache-Control "public, max-age=86400";
}
# Proxy to SvelteKit
location / {
proxy_pass http://sveltekit;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
}
```
---
## 🐳 Docker Deployment
### Dockerfile
```dockerfile
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci
# Copy source
COPY . .
# Build application
RUN npm run build
# Production stage
FROM node:20-alpine
WORKDIR /app
# Copy built application
COPY --from=builder /app/build ./build
COPY --from=builder /app/package*.json ./
# Install production dependencies only
RUN npm ci --omit=dev
# Set environment
ENV NODE_ENV=production
ENV PORT=3000
# Expose port
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
# Run
CMD ["node", "build/index.js"]
```
### docker-compose.yml
```yaml
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- ORIGIN=https://example.com
- DATABASE_URL=postgresql://user:pass@db:5432/app
depends_on:
- db
- redis
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--spider", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
db:
image: postgres:15-alpine
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: app
restart: unless-stopped
redis:
image: redis:7-alpine
volumes:
- redis_data:/data
restart: unless-stopped
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./certs:/etc/nginx/certs:ro
depends_on:
- app
restart: unless-stopped
volumes:
postgres_data:
redis_data:
```
---
## 📦 Static Site Generation (adapter-static)
### Installation
```bash
npm install -D @sveltejs/adapter-static
```
### Configuration
```javascript
// svelte.config.js
import adapter from '@sveltejs/adapter-static';
const config = {
kit: {
adapter: adapter({
pages: 'build',
assets: 'build',
fallback: '404.html', // or 'index.html' for SPA
precompress: true,
strict: true
}),
prerender: {
entries: ['*'], // Prerender all pages
handleHttpError: 'warn'
}
}
};
export default config;
```
### Page Configuration for SSG
```typescript
// src/routes/+layout.ts
export const prerender = true; // Prerender all pages
export const ssr = true; // Enable SSR during build
// src/routes/+page.ts
export const prerender = true; // Prerender this specific page
// For dynamic routes, export entries
export function entries() {
return [
{ slug: 'post-1' },
{ slug: 'post-2' },
{ slug: 'post-3' }
];
}
```
### Deploy to GitHub Pages
```yaml
# .github/workflows/deploy.yml
name: Deploy to GitHub Pages
on:
push:
branches: [main]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
env:
BASE_PATH: '/${{ github.event.repository.name }}'
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./build
```
---
## ☁️ Vercel Deployment
### Configuration
```javascript
// svelte.config.js
import adapter from '@sveltejs/adapter-vercel';
const config = {
kit: {
adapter: adapter({
runtime: 'nodejs20.x', // or 'edge'
regions: ['iad1'], // Deploy regions
split: true // Split into multiple functions
})
}
};
```
### Per-Route Configuration
```typescript
// src/routes/api/slow-endpoint/+server.ts
export const config = {
runtime: 'nodejs20.x',
maxDuration: 30 // Extend timeout
};
// src/routes/api/fast-endpoint/+server.ts
export const config = {
runtime: 'edge',
regions: ['iad1', 'sfo1', 'cdg1'] // Multi-region
};
```
### vercel.json
```json
{
"framework": "sveltekit",
"buildCommand": "npm run build",
"installCommand": "npm ci",
"headers": [
{
"source": "/_app/immutable/(.*)",
"headers": [
{
"key": "Cache-Control",
"value": "public, max-age=31536000, immutable"
}
]
}
]
}
```
---
## 🌐 Cloudflare Pages
### Configuration
```javascript
// svelte.config.js
import adapter from '@sveltejs/adapter-cloudflare';
const config = {
kit: {
adapter: adapter({
routes: {
include: ['/*'],
exclude: ['<all>']
}
})
}
};
```
### Platform Bindings
```typescript
// src/app.d.ts
declare global {
namespace App {
interface Platform {
env: {
KV_NAMESPACE: KVNamespace;
D1_DATABASE: D1Database;
R2_BUCKET: R2Bucket;
};
context: {
waitUntil(promise: Promise<unknown>): void;
};
}
}
}
// src/routes/+page.server.ts
export async function load({ platform }) {
const value = await platform?.env.KV_NAMESPACE.get('key');
return { value };
}
```
---
## 🔒 Security Considerations
### Environment Variables
```typescript
// src/lib/server/env.ts
import { env } from '$env/dynamic/private';
import { building } from '$app/environment';
if (!building) {
// Validate required env vars at startup
const required = ['DATABASE_URL', 'SECRET_KEY'];
for (const key of required) {
if (!env[key]) {
throw new Error(`Missing required environment variable: ${key}`);
}
}
}
export const config = {
databaseUrl: env.DATABASE_URL,
secretKey: env.SECRET_KEY,
isProduction: env.NODE_ENV === 'production'
};
```
### Security Headers
```typescript
// src/hooks.server.ts
import type { Handle } from '@sveltejs/kit';
export const handle: Handle = async ({ event, resolve }) => {
const response = await resolve(event);
// Security headers
response.headers.set('X-Frame-Options', 'SAMEORIGIN');
response.headers.set('X-Content-Type-Options', 'nosniff');
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
response.headers.set(
'Content-Security-Policy',
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"
);
response.headers.set(
'Permissions-Policy',
'camera=(), microphone=(), geolocation=()'
);
return response;
};
```
### CORS Configuration
```typescript
// src/routes/api/[...path]/+server.ts
import type { RequestHandler } from './$types';
const allowedOrigins = ['https://example.com', 'https://app.example.com'];
export const OPTIONS: RequestHandler = async ({ request }) => {
const origin = request.headers.get('origin');
if (origin && allowedOrigins.includes(origin)) {
return new Response(null, {
headers: {
'Access-Control-Allow-Origin': origin,
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '86400'
}
});
}
return new Response(null, { status: 403 });
};
```
---
## 📊 Health Checks & Monitoring
### Health Endpoint
```typescript
// src/routes/health/+server.ts
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async () => {
const health = {
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
memory: process.memoryUsage(),
version: process.env.npm_package_version
};
// Add database check
try {
// await db.query('SELECT 1');
health.database = 'connected';
} catch {
health.database = 'disconnected';
health.status = 'degraded';
}
const statusCode = health.status === 'healthy' ? 200 : 503;
return json(health, { status: statusCode });
};
```
---
## 📋 Deployment Checklist
### Pre-Deployment
- [ ] All tests passing
- [ ] Environment variables documented
- [ ] Build succeeds locally
- [ ] Security headers configured
- [ ] Error pages customized
- [ ] Logging configured
### Production
- [ ] SSL/TLS enabled
- [ ] CDN configured
- [ ] Database backups scheduled
- [ ] Monitoring/alerting setup
- [ ] Rate limiting enabled
- [ ] Health checks configured
### Post-Deployment
- [ ] Smoke tests passing
- [ ] Performance metrics acceptable
- [ ] Error tracking working
- [ ] Rollback plan documented
+705
View File
@@ -0,0 +1,705 @@
# Svelte 5 Migration Guide
## Overview
This guide covers migrating from Svelte 4 to Svelte 5, with focus on the new Runes system which replaces the old reactive syntax.
---
## 🔄 Key Changes at a Glance
| Svelte 4 | Svelte 5 | Description |
|----------|----------|-------------|
| `let count = 0` | `let count = $state(0)` | Reactive state |
| `$: doubled = count * 2` | `let doubled = $derived(count * 2)` | Derived values |
| `$: { sideEffect() }` | `$effect(() => { sideEffect() })` | Side effects |
| `export let prop` | `let { prop } = $props()` | Component props |
| `<slot />` | `{@render children()}` | Rendering children |
| `<slot name="x" />` | `{@render x?.()}` | Named slots → Snippets |
| `$$props`, `$$restProps` | `$props()` with rest | All props access |
| `createEventDispatcher()` | Callback props | Event handling |
---
## 📦 State Migration
### Basic State
```svelte
<!-- ❌ Svelte 4 -->
<script>
let count = 0;
function increment() {
count += 1;
}
</script>
<!-- ✅ Svelte 5 -->
<script>
let count = $state(0);
function increment() {
count += 1;
}
</script>
```
### Object/Array State
```svelte
<!-- ❌ Svelte 4 - Needed reassignment for reactivity -->
<script>
let items = [];
function addItem(item) {
items = [...items, item]; // Had to reassign
}
let user = { name: '', age: 0 };
function updateName(name) {
user = { ...user, name }; // Had to spread
}
</script>
<!-- ✅ Svelte 5 - Deep reactivity -->
<script>
let items = $state([]);
function addItem(item) {
items.push(item); // Direct mutation works!
}
let user = $state({ name: '', age: 0 });
function updateName(name) {
user.name = name; // Direct property assignment works!
}
</script>
```
---
## 🧮 Derived Values Migration
### Simple Derived
```svelte
<!-- ❌ Svelte 4 -->
<script>
let count = 0;
$: doubled = count * 2;
$: tripled = count * 3;
$: total = doubled + tripled;
</script>
<!-- ✅ Svelte 5 -->
<script>
let count = $state(0);
let doubled = $derived(count * 2);
let tripled = $derived(count * 3);
let total = $derived(doubled + tripled);
</script>
```
### Complex Derived
```svelte
<!-- ❌ Svelte 4 -->
<script>
let items = [];
let filter = '';
$: filteredItems = items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
$: {
// Complex computation
let result = [];
for (const item of items) {
if (item.active) {
result.push(processItem(item));
}
}
processedItems = result;
}
</script>
<!-- ✅ Svelte 5 -->
<script>
let items = $state([]);
let filter = $state('');
let filteredItems = $derived(
items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
)
);
// For complex computations, use $derived.by()
let processedItems = $derived.by(() => {
let result = [];
for (const item of items) {
if (item.active) {
result.push(processItem(item));
}
}
return result;
});
</script>
```
---
## ⚡ Effects Migration
### Basic Effects
```svelte
<!-- ❌ Svelte 4 -->
<script>
let count = 0;
// Reactive statement as effect
$: console.log('Count changed:', count);
// Effect with cleanup wasn't possible inline
import { onMount, onDestroy } from 'svelte';
let interval;
onMount(() => {
interval = setInterval(() => count++, 1000);
});
onDestroy(() => clearInterval(interval));
</script>
<!-- ✅ Svelte 5 -->
<script>
let count = $state(0);
// Effect automatically tracks dependencies
$effect(() => {
console.log('Count changed:', count);
});
// Effect with cleanup
$effect(() => {
const interval = setInterval(() => count++, 1000);
return () => clearInterval(interval); // Cleanup function
});
</script>
```
### Pre-Effects (beforeUpdate replacement)
```svelte
<!-- ❌ Svelte 4 -->
<script>
import { beforeUpdate, afterUpdate } from 'svelte';
let messages = [];
let container;
let autoscroll = false;
beforeUpdate(() => {
autoscroll = container &&
container.scrollTop + container.clientHeight >= container.scrollHeight - 20;
});
afterUpdate(() => {
if (autoscroll) {
container.scrollTo(0, container.scrollHeight);
}
});
</script>
<!-- ✅ Svelte 5 -->
<script>
let messages = $state([]);
let container = $state();
$effect.pre(() => {
// Runs before DOM updates
if (!container) return;
// ... pre-update logic
});
$effect(() => {
// Runs after DOM updates
if (!container) return;
container.scrollTo(0, container.scrollHeight);
});
</script>
```
---
## 🎁 Props Migration
### Basic Props
```svelte
<!-- ❌ Svelte 4 -->
<script>
export let name;
export let count = 0; // With default
export let optional = undefined;
</script>
<!-- ✅ Svelte 5 -->
<script>
let {
name,
count = 0,
optional
} = $props();
</script>
```
### Rest Props
```svelte
<!-- ❌ Svelte 4 -->
<script>
export let variant = 'primary';
// $$restProps used in template
</script>
<button class={variant} {...$$restProps}>
<slot />
</button>
<!-- ✅ Svelte 5 -->
<script>
let {
variant = 'primary',
children,
...restProps
} = $props();
</script>
<button class={variant} {...restProps}>
{@render children?.()}
</button>
```
### Bindable Props (Two-way Binding)
```svelte
<!-- ❌ Svelte 4 - Parent -->
<script>
let value = '';
</script>
<Input bind:value />
<!-- ❌ Svelte 4 - Child (Input.svelte) -->
<script>
export let value;
</script>
<input bind:value />
<!-- ✅ Svelte 5 - Parent (same) -->
<script>
let value = $state('');
</script>
<Input bind:value />
<!-- ✅ Svelte 5 - Child (Input.svelte) -->
<script>
let { value = $bindable() } = $props();
</script>
<input bind:value />
```
---
## 🎰 Slots → Snippets Migration
### Default Slot
```svelte
<!-- ❌ Svelte 4 - Card.svelte -->
<div class="card">
<slot />
</div>
<!-- ❌ Svelte 4 - Usage -->
<Card>
<p>Card content</p>
</Card>
<!-- ✅ Svelte 5 - Card.svelte -->
<script>
let { children } = $props();
</script>
<div class="card">
{@render children?.()}
</div>
<!-- ✅ Svelte 5 - Usage -->
<Card>
<p>Card content</p>
</Card>
```
### Named Slots
```svelte
<!-- ❌ Svelte 4 - Layout.svelte -->
<header>
<slot name="header" />
</header>
<main>
<slot />
</main>
<footer>
<slot name="footer" />
</footer>
<!-- ❌ Svelte 4 - Usage -->
<Layout>
<h1 slot="header">Title</h1>
<p>Main content</p>
<span slot="footer">Footer</span>
</Layout>
<!-- ✅ Svelte 5 - Layout.svelte -->
<script>
import type { Snippet } from 'svelte';
let {
header,
children,
footer
}: {
header?: Snippet;
children?: Snippet;
footer?: Snippet;
} = $props();
</script>
<header>
{@render header?.()}
</header>
<main>
{@render children?.()}
</main>
<footer>
{@render footer?.()}
</footer>
<!-- ✅ Svelte 5 - Usage -->
<Layout>
{#snippet header()}
<h1>Title</h1>
{/snippet}
<p>Main content</p>
{#snippet footer()}
<span>Footer</span>
{/snippet}
</Layout>
```
### Slot Props → Snippet Parameters
```svelte
<!-- ❌ Svelte 4 - List.svelte -->
<script>
export let items = [];
</script>
<ul>
{#each items as item}
<li>
<slot {item} index={items.indexOf(item)} />
</li>
{/each}
</ul>
<!-- ❌ Svelte 4 - Usage -->
<List {items} let:item let:index>
<span>{index}: {item.name}</span>
</List>
<!-- ✅ Svelte 5 - List.svelte -->
<script>
import type { Snippet } from 'svelte';
let {
items,
children
}: {
items: Item[];
children: Snippet<[{ item: Item; index: number }]>;
} = $props();
</script>
<ul>
{#each items as item, index}
<li>
{@render children({ item, index })}
</li>
{/each}
</ul>
<!-- ✅ Svelte 5 - Usage -->
<List {items}>
{#snippet children({ item, index })}
<span>{index}: {item.name}</span>
{/snippet}
</List>
```
---
## 📡 Events Migration
### Custom Events
```svelte
<!-- ❌ Svelte 4 - Button.svelte -->
<script>
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
function handleClick() {
dispatch('click', { timestamp: Date.now() });
}
</script>
<button on:click={handleClick}>
<slot />
</button>
<!-- ❌ Svelte 4 - Usage -->
<Button on:click={(e) => console.log(e.detail.timestamp)}>
Click me
</Button>
<!-- ✅ Svelte 5 - Button.svelte -->
<script>
let {
onclick,
children
}: {
onclick?: (data: { timestamp: number }) => void;
children: Snippet;
} = $props();
function handleClick() {
onclick?.({ timestamp: Date.now() });
}
</script>
<button onclick={handleClick}>
{@render children()}
</button>
<!-- ✅ Svelte 5 - Usage -->
<Button onclick={(data) => console.log(data.timestamp)}>
Click me
</Button>
```
### Event Forwarding
```svelte
<!-- ❌ Svelte 4 - Wrapper.svelte -->
<button on:click on:mouseenter on:mouseleave>
<slot />
</button>
<!-- ✅ Svelte 5 - Wrapper.svelte -->
<script>
let {
onclick,
onmouseenter,
onmouseleave,
children,
...restProps
} = $props();
</script>
<!-- Option 1: Explicit forwarding -->
<button {onclick} {onmouseenter} {onmouseleave}>
{@render children?.()}
</button>
<!-- Option 2: Spread all props -->
<button {...restProps}>
{@render children?.()}
</button>
```
---
## 🏪 Stores Migration
### Writable Stores
```svelte
<!-- ❌ Svelte 4 -->
<script>
import { writable } from 'svelte/store';
const count = writable(0);
function increment() {
count.update(n => n + 1);
}
</script>
<button on:click={increment}>
Count: {$count}
</button>
<!-- ✅ Svelte 5 - Using class-based state -->
// stores/counter.svelte.ts
export class Counter {
count = $state(0);
increment() {
this.count++;
}
}
export const counter = new Counter();
// Component
<script>
import { counter } from './stores/counter.svelte';
</script>
<button onclick={() => counter.increment()}>
Count: {counter.count}
</button>
```
### Derived Stores
```svelte
<!-- ❌ Svelte 4 -->
<script>
import { writable, derived } from 'svelte/store';
const count = writable(0);
const doubled = derived(count, $count => $count * 2);
</script>
<p>Doubled: {$doubled}</p>
<!-- ✅ Svelte 5 -->
// stores/counter.svelte.ts
export class Counter {
count = $state(0);
doubled = $derived(this.count * 2);
}
// Component
<script>
import { counter } from './stores/counter.svelte';
</script>
<p>Doubled: {counter.doubled}</p>
```
---
## 🔧 Component Lifecycle
### onMount / onDestroy
```svelte
<!-- ❌ Svelte 4 -->
<script>
import { onMount, onDestroy } from 'svelte';
let data;
let subscription;
onMount(async () => {
data = await fetchData();
subscription = subscribe();
return () => {
subscription.unsubscribe();
};
});
onDestroy(() => {
cleanup();
});
</script>
<!-- ✅ Svelte 5 - onMount still works! -->
<script>
import { onMount } from 'svelte';
let data = $state(null);
onMount(async () => {
data = await fetchData();
});
// For subscriptions, use $effect
$effect(() => {
const subscription = subscribe();
return () => subscription.unsubscribe();
});
</script>
```
---
## 📋 Migration Checklist
### Phase 1: Preparation
- [ ] Update to latest Svelte 4.x
- [ ] Fix all deprecation warnings
- [ ] Ensure tests are passing
- [ ] Review component inventory
### Phase 2: Upgrade
- [ ] Update to Svelte 5
- [ ] Update SvelteKit (if using)
- [ ] Update dependencies
### Phase 3: Migrate Components
- [ ] Convert `export let``$props()`
- [ ] Convert reactive declarations → `$derived()`
- [ ] Convert reactive statements → `$effect()`
- [ ] Convert slots → snippets
- [ ] Convert events → callback props
- [ ] Convert stores → class-based state
### Phase 4: Validation
- [ ] Run all tests
- [ ] Check for runtime errors
- [ ] Performance testing
- [ ] Accessibility audit
---
## 🛠️ Automated Migration
Svelte 5 includes a migration script:
```bash
# Run the migration script
npx sv migrate svelte-5
# This will:
# - Update syntax to Svelte 5
# - Convert reactive statements
# - Update event handlers
# - Flag manual review items
```
**Note:** Always review automated changes and test thoroughly!
+735
View File
@@ -0,0 +1,735 @@
# Svelte Common Utilities & Helpers
## Overview
A collection of reusable utilities, helpers, and patterns commonly needed in Svelte 5 applications.
---
## 🎣 Custom Hooks (Reactive Utilities)
### useLocalStorage
```typescript
// src/lib/hooks/useLocalStorage.svelte.ts
import { browser } from '$app/environment';
export function useLocalStorage<T>(key: string, initialValue: T) {
let value = $state<T>(initialValue);
// Load from localStorage on init
if (browser) {
const stored = localStorage.getItem(key);
if (stored) {
try {
value = JSON.parse(stored);
} catch {
value = initialValue;
}
}
}
// Sync to localStorage on change
$effect(() => {
if (browser) {
localStorage.setItem(key, JSON.stringify(value));
}
});
return {
get value() { return value; },
set value(v: T) { value = v; },
reset() { value = initialValue; }
};
}
// Usage
const theme = useLocalStorage('theme', 'light');
// theme.value = 'dark';
```
### useDebounce
```typescript
// src/lib/hooks/useDebounce.svelte.ts
export function useDebounce<T>(getValue: () => T, delay: number = 300) {
let debouncedValue = $state<T>(getValue());
let timeout: ReturnType<typeof setTimeout>;
$effect(() => {
const currentValue = getValue();
clearTimeout(timeout);
timeout = setTimeout(() => {
debouncedValue = currentValue;
}, delay);
return () => clearTimeout(timeout);
});
return {
get value() { return debouncedValue; }
};
}
// Usage
let searchQuery = $state('');
const debouncedQuery = useDebounce(() => searchQuery, 300);
// Use debouncedQuery.value for API calls
```
### useThrottle
```typescript
// src/lib/hooks/useThrottle.svelte.ts
export function useThrottle<T>(getValue: () => T, limit: number = 300) {
let throttledValue = $state<T>(getValue());
let lastRun = 0;
$effect(() => {
const currentValue = getValue();
const now = Date.now();
if (now - lastRun >= limit) {
throttledValue = currentValue;
lastRun = now;
}
});
return {
get value() { return throttledValue; }
};
}
```
### useMediaQuery
```typescript
// src/lib/hooks/useMediaQuery.svelte.ts
import { browser } from '$app/environment';
export function useMediaQuery(query: string) {
let matches = $state(false);
$effect(() => {
if (!browser) return;
const mediaQuery = window.matchMedia(query);
matches = mediaQuery.matches;
const handler = (e: MediaQueryListEvent) => {
matches = e.matches;
};
mediaQuery.addEventListener('change', handler);
return () => mediaQuery.removeEventListener('change', handler);
});
return {
get matches() { return matches; }
};
}
// Usage
const isMobile = useMediaQuery('(max-width: 768px)');
// {#if isMobile.matches}...{/if}
```
### useOnline
```typescript
// src/lib/hooks/useOnline.svelte.ts
import { browser } from '$app/environment';
export function useOnline() {
let isOnline = $state(browser ? navigator.onLine : true);
$effect(() => {
if (!browser) return;
const handleOnline = () => { isOnline = true; };
const handleOffline = () => { isOnline = false; };
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
});
return {
get isOnline() { return isOnline; }
};
}
```
### useClickOutside
```typescript
// src/lib/hooks/useClickOutside.svelte.ts
export function useClickOutside(
getElement: () => HTMLElement | null,
callback: () => void
) {
$effect(() => {
const element = getElement();
if (!element) return;
const handler = (event: MouseEvent) => {
if (!element.contains(event.target as Node)) {
callback();
}
};
document.addEventListener('click', handler);
return () => document.removeEventListener('click', handler);
});
}
// Usage
let menuRef = $state<HTMLElement>();
let isOpen = $state(false);
useClickOutside(
() => menuRef,
() => { isOpen = false; }
);
```
### useIntersectionObserver
```typescript
// src/lib/hooks/useIntersectionObserver.svelte.ts
interface UseIntersectionOptions {
threshold?: number | number[];
root?: Element | null;
rootMargin?: string;
}
export function useIntersectionObserver(
getElement: () => HTMLElement | null,
options: UseIntersectionOptions = {}
) {
let isIntersecting = $state(false);
let entry = $state<IntersectionObserverEntry | null>(null);
$effect(() => {
const element = getElement();
if (!element) return;
const observer = new IntersectionObserver(
([e]) => {
isIntersecting = e.isIntersecting;
entry = e;
},
{
threshold: options.threshold ?? 0,
root: options.root ?? null,
rootMargin: options.rootMargin ?? '0px'
}
);
observer.observe(element);
return () => observer.disconnect();
});
return {
get isIntersecting() { return isIntersecting; },
get entry() { return entry; }
};
}
// Usage - Lazy loading
let imageRef = $state<HTMLElement>();
const { isIntersecting } = useIntersectionObserver(() => imageRef);
// Load image when isIntersecting becomes true
```
---
## 🛠️ Utility Functions
### Class Name Utilities
```typescript
// src/lib/utils/cn.ts
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
// Usage
cn('px-4 py-2', isActive && 'bg-blue-500', className)
```
### Format Utilities
```typescript
// src/lib/utils/format.ts
// Currency formatting
export function formatCurrency(
amount: number,
currency = 'USD',
locale = 'en-US'
): string {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency
}).format(amount);
}
// Date formatting
export function formatDate(
date: Date | string | number,
options: Intl.DateTimeFormatOptions = {},
locale = 'en-US'
): string {
const d = date instanceof Date ? date : new Date(date);
return new Intl.DateTimeFormat(locale, {
dateStyle: 'medium',
...options
}).format(d);
}
// Relative time
export function formatRelativeTime(date: Date | string | number): string {
const d = date instanceof Date ? date : new Date(date);
const now = new Date();
const diffMs = d.getTime() - now.getTime();
const diffSec = Math.round(diffMs / 1000);
const diffMin = Math.round(diffSec / 60);
const diffHour = Math.round(diffMin / 60);
const diffDay = Math.round(diffHour / 24);
const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
if (Math.abs(diffSec) < 60) return rtf.format(diffSec, 'second');
if (Math.abs(diffMin) < 60) return rtf.format(diffMin, 'minute');
if (Math.abs(diffHour) < 24) return rtf.format(diffHour, 'hour');
return rtf.format(diffDay, 'day');
}
// Number formatting
export function formatNumber(
num: number,
options: Intl.NumberFormatOptions = {}
): string {
return new Intl.NumberFormat('en-US', options).format(num);
}
// Compact number (1K, 1M, etc.)
export function formatCompact(num: number): string {
return new Intl.NumberFormat('en-US', {
notation: 'compact',
compactDisplay: 'short'
}).format(num);
}
// File size
export function formatFileSize(bytes: number): string {
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
let size = bytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return `${size.toFixed(1)} ${units[unitIndex]}`;
}
```
### String Utilities
```typescript
// src/lib/utils/string.ts
// Truncate text
export function truncate(str: string, length: number, ending = '...'): string {
if (str.length <= length) return str;
return str.slice(0, length - ending.length) + ending;
}
// Slugify
export function slugify(str: string): string {
return str
.toLowerCase()
.trim()
.replace(/[^\w\s-]/g, '')
.replace(/[\s_-]+/g, '-')
.replace(/^-+|-+$/g, '');
}
// Capitalize
export function capitalize(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1);
}
// Title case
export function titleCase(str: string): string {
return str
.toLowerCase()
.split(' ')
.map(word => capitalize(word))
.join(' ');
}
// Pluralize
export function pluralize(
count: number,
singular: string,
plural?: string
): string {
return count === 1 ? singular : (plural ?? `${singular}s`);
}
```
### Array Utilities
```typescript
// src/lib/utils/array.ts
// Group by key
export function groupBy<T>(
array: T[],
key: keyof T | ((item: T) => string)
): Record<string, T[]> {
return array.reduce((groups, item) => {
const groupKey = typeof key === 'function' ? key(item) : String(item[key]);
groups[groupKey] = [...(groups[groupKey] || []), item];
return groups;
}, {} as Record<string, T[]>);
}
// Unique values
export function unique<T>(array: T[]): T[] {
return [...new Set(array)];
}
// Unique by key
export function uniqueBy<T>(array: T[], key: keyof T): T[] {
const seen = new Set();
return array.filter(item => {
const k = item[key];
if (seen.has(k)) return false;
seen.add(k);
return true;
});
}
// Chunk array
export function chunk<T>(array: T[], size: number): T[][] {
const chunks: T[][] = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
// Shuffle array
export function shuffle<T>(array: T[]): T[] {
const result = [...array];
for (let i = result.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[result[i], result[j]] = [result[j], result[i]];
}
return result;
}
// Sort by key
export function sortBy<T>(
array: T[],
key: keyof T,
order: 'asc' | 'desc' = 'asc'
): T[] {
return [...array].sort((a, b) => {
const aVal = a[key];
const bVal = b[key];
const compare = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
return order === 'asc' ? compare : -compare;
});
}
```
### Object Utilities
```typescript
// src/lib/utils/object.ts
// Deep clone
export function deepClone<T>(obj: T): T {
return structuredClone(obj);
}
// Pick keys
export function pick<T extends object, K extends keyof T>(
obj: T,
keys: K[]
): Pick<T, K> {
return keys.reduce((result, key) => {
if (key in obj) result[key] = obj[key];
return result;
}, {} as Pick<T, K>);
}
// Omit keys
export function omit<T extends object, K extends keyof T>(
obj: T,
keys: K[]
): Omit<T, K> {
const result = { ...obj };
for (const key of keys) {
delete result[key];
}
return result;
}
// Deep equal
export function deepEqual(a: unknown, b: unknown): boolean {
if (a === b) return true;
if (typeof a !== typeof b) return false;
if (a == null || b == null) return false;
if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length) return false;
return a.every((val, i) => deepEqual(val, b[i]));
}
if (typeof a === 'object' && typeof b === 'object') {
const keysA = Object.keys(a);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;
return keysA.every(key =>
deepEqual((a as any)[key], (b as any)[key])
);
}
return false;
}
```
---
## 🎯 Svelte Actions
### Focus Trap
```typescript
// src/lib/actions/focusTrap.ts
export function focusTrap(node: HTMLElement) {
const focusableElements = node.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const first = focusableElements[0] as HTMLElement;
const last = focusableElements[focusableElements.length - 1] as HTMLElement;
function handleKeydown(e: KeyboardEvent) {
if (e.key !== 'Tab') return;
if (e.shiftKey) {
if (document.activeElement === first) {
last.focus();
e.preventDefault();
}
} else {
if (document.activeElement === last) {
first.focus();
e.preventDefault();
}
}
}
node.addEventListener('keydown', handleKeydown);
first?.focus();
return {
destroy() {
node.removeEventListener('keydown', handleKeydown);
}
};
}
```
### Tooltip
```typescript
// src/lib/actions/tooltip.ts
interface TooltipOptions {
text: string;
position?: 'top' | 'bottom' | 'left' | 'right';
}
export function tooltip(node: HTMLElement, options: TooltipOptions) {
let tooltipEl: HTMLDivElement | null = null;
function show() {
tooltipEl = document.createElement('div');
tooltipEl.className = `tooltip tooltip-${options.position ?? 'top'}`;
tooltipEl.textContent = options.text;
document.body.appendChild(tooltipEl);
const rect = node.getBoundingClientRect();
const tipRect = tooltipEl.getBoundingClientRect();
// Position based on option
switch (options.position ?? 'top') {
case 'top':
tooltipEl.style.left = `${rect.left + rect.width / 2 - tipRect.width / 2}px`;
tooltipEl.style.top = `${rect.top - tipRect.height - 8}px`;
break;
case 'bottom':
tooltipEl.style.left = `${rect.left + rect.width / 2 - tipRect.width / 2}px`;
tooltipEl.style.top = `${rect.bottom + 8}px`;
break;
// ... other positions
}
}
function hide() {
tooltipEl?.remove();
tooltipEl = null;
}
node.addEventListener('mouseenter', show);
node.addEventListener('mouseleave', hide);
return {
update(newOptions: TooltipOptions) {
options = newOptions;
},
destroy() {
hide();
node.removeEventListener('mouseenter', show);
node.removeEventListener('mouseleave', hide);
}
};
}
```
### Auto-resize Textarea
```typescript
// src/lib/actions/autoResize.ts
export function autoResize(node: HTMLTextAreaElement) {
function resize() {
node.style.height = 'auto';
node.style.height = `${node.scrollHeight}px`;
}
resize();
node.addEventListener('input', resize);
return {
destroy() {
node.removeEventListener('input', resize);
}
};
}
// Usage: <textarea use:autoResize />
```
### Copy to Clipboard
```typescript
// src/lib/actions/clipboard.ts
interface ClipboardOptions {
text: string;
onCopy?: () => void;
onError?: (error: Error) => void;
}
export function clipboard(node: HTMLElement, options: ClipboardOptions) {
async function handleClick() {
try {
await navigator.clipboard.writeText(options.text);
options.onCopy?.();
} catch (error) {
options.onError?.(error as Error);
}
}
node.addEventListener('click', handleClick);
return {
update(newOptions: ClipboardOptions) {
options = newOptions;
},
destroy() {
node.removeEventListener('click', handleClick);
}
};
}
// Usage
// <button use:clipboard={{ text: 'Copy me!', onCopy: () => showToast('Copied!') }}>
// Copy
// </button>
```
---
## 📋 Type Utilities
```typescript
// src/lib/types/utils.ts
// Make all properties optional recursively
export type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
// Make specified keys required
export type RequireKeys<T, K extends keyof T> = T & Required<Pick<T, K>>;
// Extract array element type
export type ArrayElement<T> = T extends readonly (infer U)[] ? U : never;
// Async function return type
export type AsyncReturnType<T extends (...args: any) => Promise<any>> =
T extends (...args: any) => Promise<infer R> ? R : never;
// Make all properties non-nullable
export type NonNullableProps<T> = {
[P in keyof T]: NonNullable<T[P]>;
};
// Record with known keys
export type StrictRecord<K extends string, V> = { [P in K]: V };
```
---
## 🔗 Export Index
```typescript
// src/lib/utils/index.ts
export * from './cn';
export * from './format';
export * from './string';
export * from './array';
export * from './object';
// src/lib/hooks/index.ts
export * from './useLocalStorage.svelte';
export * from './useDebounce.svelte';
export * from './useThrottle.svelte';
export * from './useMediaQuery.svelte';
export * from './useOnline.svelte';
export * from './useClickOutside.svelte';
export * from './useIntersectionObserver.svelte';
// src/lib/actions/index.ts
export * from './focusTrap';
export * from './tooltip';
export * from './autoResize';
export * from './clipboard';
```
+178
View File
@@ -0,0 +1,178 @@
# Svelte Expert Persona & Instructions
## 🎭 Persona: Svelte Architect
I am a **Svelte 5 Expert** with deep knowledge of:
- Svelte 5 Runes (`$state`, `$derived`, `$effect`, `$props`, `$bindable`)
- SvelteKit for full-stack applications
- Component architecture and composition patterns
- Reactive programming paradigms
- Performance optimization techniques
- TypeScript integration with Svelte
- Modern CSS/Tailwind integration
---
## 🎯 Core Principles
### 1. **Runes-First Reactivity (Svelte 5)**
```svelte
<script lang="ts">
// ✅ Use Runes for all reactive state
let count = $state(0);
let doubled = $derived(count * 2);
$effect(() => {
console.log(`Count changed to ${count}`);
});
</script>
```
### 2. **Props with `$props()` Rune**
```svelte
<script lang="ts">
// ✅ Svelte 5 way
let { name, age = 18, onclick }: {
name: string;
age?: number;
onclick?: () => void
} = $props();
// ❌ OLD Svelte 4 way - DO NOT USE
// export let name;
</script>
```
### 3. **Bindable Props with `$bindable()`**
```svelte
<script lang="ts">
let { value = $bindable('') }: { value?: string } = $props();
</script>
<input bind:value />
```
### 4. **Component Composition Over Inheritance**
```svelte
<!-- ✅ Use slots and snippets for composition -->
<Card>
{#snippet header()}
<h2>Title</h2>
{/snippet}
<p>Content goes here</p>
</Card>
```
---
## 📋 Mandatory Checklist
Before writing any Svelte code, verify:
- [ ] Using Svelte 5 syntax (Runes, not `export let`)
- [ ] TypeScript enabled (`lang="ts"`)
- [ ] Props typed with TypeScript interfaces
- [ ] State managed with `$state()` rune
- [ ] Derived values use `$derived()` rune
- [ ] Side effects wrapped in `$effect()`
- [ ] Components are single-responsibility
- [ ] CSS is scoped or uses Tailwind
- [ ] Accessibility attributes included
- [ ] Error boundaries considered
---
## 🚫 Anti-Patterns to Avoid
### ❌ Never Do This:
```svelte
<!-- OLD Svelte 4 syntax -->
<script>
export let name; // ❌ Use $props() instead
$: doubled = count * 2; // ❌ Use $derived() instead
$: { console.log(count); } // ❌ Use $effect() instead
</script>
```
### ✅ Always Do This:
```svelte
<script lang="ts">
let { name }: { name: string } = $props();
let count = $state(0);
let doubled = $derived(count * 2);
$effect(() => {
console.log(count);
});
</script>
```
---
## 🔧 Tools & Commands
### Validation Commands
```bash
# Type check Svelte files
npx svelte-check --tsconfig ./tsconfig.json
# Build with strict mode
npm run build
# Run dev server
npm run dev
```
### VS Code Extensions Required
- Svelte for VS Code (official)
- Svelte Intellisense
- TypeScript + JavaScript Language Features
---
## 📁 File Naming Conventions
```
src/
├── lib/
│ ├── components/ # Reusable UI components
│ │ ├── Button.svelte
│ │ ├── Card.svelte
│ │ └── Modal.svelte
│ ├── stores/ # Global state stores
│ │ └── auth.svelte.ts
│ ├── utils/ # Utility functions
│ │ └── formatters.ts
│ └── types/ # TypeScript types
│ └── index.ts
├── routes/ # SvelteKit routes
│ ├── +page.svelte
│ ├── +layout.svelte
│ └── api/
└── app.html
```
---
## 🔗 Quick Reference Links
- [Svelte 5 Docs](https://svelte.dev/docs/svelte/overview)
- [SvelteKit Docs](https://svelte.dev/docs/kit/introduction)
- [Svelte 5 Runes](https://svelte.dev/docs/svelte/$state)
- [Svelte Tutorial](https://learn.svelte.dev/)
---
## 📞 How to Use This Persona
When asking for Svelte help, I will:
1. **Always use Svelte 5 syntax** with Runes
2. **Provide TypeScript-first examples**
3. **Follow component best practices**
4. **Optimize for performance**
5. **Include accessibility considerations**
6. **Reference this guide for consistency**
Invoke with: *"As the Svelte Expert, [your question]"*