Create UI Route or Component
Use this skill when the user asks to add a new page, route, or component to the SvelteKit frontend.
Creating a new route
Routes live in ui/src/routes/. SvelteKit uses file-based routing.
Route file structure
ui/src/routes/<route-name>/
├── +page.svelte # Page component
├── +page.ts # Load function (optional, for SSR data)
└── +layout.svelte # Layout wrapper (optional, inherits parent)
Page template (Svelte 5 runes)
svelte
1<script lang="ts">
2 import { onMount } from 'svelte';
3 import { API_BASE, type MyType } from '$lib/api';
4
5 // State — use $state(), never Svelte 4 stores
6 let items: MyType[] = $state([]);
7 let loading = $state(true);
8 let error = $state('');
9
10 // Derived values — use $derived()
11 let itemCount = $derived(items.length);
12 let hasItems = $derived(items.length > 0);
13
14 // Complex derived — use $derived.by()
15 let summary = $derived.by(() => {
16 return items.reduce((acc, item) => acc + item.value, 0);
17 });
18
19 // Side effects — use $effect()
20 $effect(() => {
21 console.log(`Items changed: ${items.length}`);
22 });
23
24 onMount(async () => {
25 try {
26 const res = await fetch(`${API_BASE}/internal/my-endpoint?org_id=...&project_id=...`);
27 items = await res.json();
28 } catch (e: any) {
29 error = e?.message || 'Failed to load';
30 }
31 loading = false;
32 });
33</script>
34
35<div class="app-shell-wide">
36 {#if loading}
37 <p class="text-zinc-500 text-sm">Loading...</p>
38 {:else if error}
39 <div class="alert-danger">{error}</div>
40 {:else}
41 <div class="table-float">
42 <!-- Content here -->
43 </div>
44 {/if}
45</div>
Creating a component
Components live in ui/src/lib/components/.
svelte
1<script lang="ts">
2 // Props — use $props(), never export let
3 let {
4 items = [],
5 selected = null,
6 onSelect = undefined,
7 }: {
8 items: Item[];
9 selected?: string | null;
10 onSelect?: (id: string) => void;
11 } = $props();
12
13 // Internal state
14 let expanded = $state(false);
15</script>
16
17<div class="surface-panel">
18 {#each items as item (item.id)}
19 <button
20 class="btn-ghost"
21 class:active={selected === item.id}
22 onclick={() => onSelect?.(item.id)}
23 >
24 {item.name}
25 </button>
26 {/each}
27</div>
Adding API fetch helpers
Add new fetch functions to ui/src/lib/api.ts:
typescript
1export async function getMyEntities(): Promise<MyEntity[]> {
2 const res = await fetch(`${API_BASE}/internal/my-entities?org_id=${getOrgId()}&project_id=${getProjectId()}`);
3 if (!res.ok) throw new Error(`Failed to fetch: ${res.statusText}`);
4 const data = await res.json();
5 return data.items;
6}
If the type comes from the OpenAPI spec, re-export it from api-types.ts:
typescript
1export type MyEntity = Schemas['MyEntity'];
Design system primitives
Always check DESIGN_SYSTEM.md and reuse existing classes before adding new styles:
- Surfaces:
surface-panel, surface-command, surface-quiet, table-float
- Shells:
app-shell-wide, app-toolbar-shell, app-page-shell
- Controls:
control-input, control-select, control-textarea
- Buttons:
btn-primary, btn-secondary, btn-ghost
- Chips:
query-chip, query-chip-active
- Labels:
label-micro, table-head-compact
- Alerts:
alert-danger, alert-success, alert-warning
Key conventions
- Svelte 5 only:
$state, $derived, $effect, $props — never use Svelte 4 writable/derived stores
- Tailwind CSS v4: Classes in markup, no CSS modules
- Dark theme first: Maintain dark/light parity on major surfaces
- Dense controls: 11px micro labels, 13px body, 14-16px headings
- Floating panels: Use right floating panel for detail/edit flows
- a11y: Use
for/id on label/select pairs, proper button elements
- Page params:
page.params.id can be undefined in Svelte 5 — always default with ?? ''
- Type casting: Cast filters for query strings:
(filter ?? {}) as Record<string, string | undefined>
After creating the route
- Run svelte-check:
cd ui && npm run check
- Verify in browser at
http://localhost:5173/<route-name>