---
name: Windows and SvelteKit routes
type: architecture-map-page
slice: 01-frontend
last_verified: 2026/05/09
---

# Windows and routes

> **Where you are:** [Architecture map](../README.md) → [Frontend](README.md) → Windows and routes

**Plain English summary.** Lumotia is a single SvelteKit build that runs as multiple desktop windows. The Rust side opens four webviews (main, tasks float, transcript viewer, transcription preview) and points each one at a different SvelteKit route. The frontend uses SvelteKit's `+layout@.svelte` "break" trick so secondary windows skip the main shell (sidebar, titlebar, toast viewport) and render a focused, single purpose UI.

## At a glance

- **Path:** `src/routes/`
- **LOC:** 2 740 across the route tree.
- **Key files:**
  - `src/routes/+layout.js:5`. Disables SSR. Adapter is `adapter-static` with `index.html` fallback (`svelte.config.js:6`). The whole tree is SPA only.
  - `src/routes/+layout.svelte`. The shell. Sidebar, custom titlebar (non Linux only), toast viewport, focus timer overlay, morning triage modal, resize handles, global hotkey wiring, OS detection, profile load, first run gate, error capture.
  - `src/routes/+page.svelte`. The main window's page switch (`page.current` → 7 page modules).
  - `src/routes/float/+layout@.svelte`. "Break" layout for the tasks float window.
  - `src/routes/float/+page.svelte`. Tasks float window UI.
  - `src/routes/viewer/+layout@.svelte`. Break layout for the transcript editor window.
  - `src/routes/viewer/+page.svelte`. Transcript editor with audio playback.
  - `src/routes/preview/+layout@.svelte`. Break layout for the live transcription preview overlay.
  - `src/routes/preview/+page.svelte`. Preview state machine: listening → live → cleanup → final.
- **Imports:**
  - Internal: every store, plus shell components (`Sidebar`, `TaskSidebar`, `Titlebar`, `ToastViewport`, `ResizeHandles`, `FocusTimer`, `MorningTriageModal`).
  - External: `@tauri-apps/api/core` (invoke, Channel, convertFileSrc), `@tauri-apps/api/event` (listen, emit), `@tauri-apps/api/window` (getCurrentWindow), `@tauri-apps/plugin-global-shortcut`, `@tauri-apps/plugin-dialog`, `lucide-svelte`.
- **Used by:** Tauri (slice 02). Window labels are `main`, `tasks-float`, `transcript-viewer`, `transcription-preview`. Each is created with the matching route URL.

## What's in here

### `src/routes/+layout.js` (5 LOC)

Single line: `export const ssr = false;`. Tauri has no Node server, so SvelteKit must run as a SPA.

### `src/routes/+layout.svelte` (493 LOC)

The shell. Mounts on every window (the secondary windows then render only `{@render children()}` and suppress the chrome).

Responsibilities:
- Initialise svelte-i18n (`initI18n()` is idempotent across windows, `+layout.svelte:38`).
- Detect Tauri runtime (`hasTauriRuntime`) and OS (`loadOsInfo`). Linux uses native KWin/Mutter decorations; macOS and Windows render the custom `Titlebar` plus invisible `ResizeHandles`. Default `useCustomChrome = false` to avoid a flash on Linux (`+layout.svelte:49`).
- Hotkey backend selection. On Wayland, attempt the evdev backend (`check_hotkey_access`). Otherwise fall back to `tauri-plugin-global-shortcut`. Falls back to "unavailable" if neither path works (`+layout.svelte:76-102`).
- Hotkey registration. Only the `main` window owns the global shortcut. The function debounces evdev autorepeat at 120 ms (Handy issue #1143 referenced in comments, `+layout.svelte:171-186`).
- Cross window listeners: `lumotia:hotkey-pressed` (evdev path), `lumotia:open-wind-down` (tray menu), `lumotia:preferences-changed` (sync prefs across windows). Apply external preferences if the source label differs from our own.
- Theme migration `$effect`. Reads `settings.theme` (legacy "Light" / "Dark" / "System") and writes `preferences.theme` ("light" / "dark" / "system"). See README debt note 2.
- Font size CSS variable `--font-size-transcript` set on `<body>` from `settings.fontSize`.
- Global error capture. `window.onerror` and `unhandledrejection` forward to `log_frontend_error` Rust command. Best effort, swallow throws.
- First run check on mount. If `list_models` and `list_parakeet_models` both return empty arrays, switch `page.current` to `"first-run"`.
- Background update check via `check_for_update`. Toast on result.
- Pre warm default model on mount if `settings.prewarmModelOnStartup` (calls `prewarm_default_model_cmd`).
- Meeting auto capture poller (`$effect`). When `settings.meetingAutoCapture` is true, polls `detect_meeting_processes` every 15 s with the configured patterns (default `["zoom", "teams"]`). Edge triggered (toast only on first match per session). Tear down on effect re run.
- Sidebar collapse heuristic. `[` toggles, narrow viewport collapses automatically.
- Render branches: `isSecondaryWindow` (URL starts with `/float` or `/viewer`) renders only children. Otherwise: optional Titlebar, sidebar (hidden on first run), main slot, optional task sidebar (when `page.taskSidebarOpen`).
- Always rendered alongside: `<ToastViewport />`, `<FocusTimer />`, `<MorningTriageModal />`, `<ResizeHandles />` (custom chrome only).

### `src/routes/+page.svelte` (33 LOC)

Pure dispatcher. Switches the seven page modules from `page.current`:
- `first-run` → `FirstRunPage`
- `dictation` → `DictationPage`
- `files` → `FilesPage`
- `tasks` → `TasksPage`
- `history` → `HistoryPage`
- `settings` → `SettingsPage`
- `shutdown` → `ShutdownRitualPage`

Also redirects the legacy `page.current === "profiles"` to `"settings"` via `$effect` (debt note 5).

### `src/routes/float/+layout@.svelte` (99 LOC)

The `@.svelte` suffix tells SvelteKit to skip parent layouts. Reimports `app.css`, mounts `Titlebar` (non Linux) and `FocusTimer`, applies the same theme migration `$effect`, listens on the browser `storage` event (not Tauri events) to sync `settings` from main window writes, and handles the `task-window-focus` Tauri event to glow and auto focus the quick add input.

### `src/routes/float/+page.svelte` (481 LOC)

Tasks float window. Self contained quick add, list filter, sort menu, completed toggle, list management (rename, delete, reorder, move to profile), drag and drop reordering. Reads tasks straight from the `page.svelte.ts` store helpers. No `invoke()` calls of its own; mutations go via `addTask`, `completeTask`, etc, which then call Rust.

### `src/routes/viewer/+layout@.svelte` (77 LOC)

Same break layout pattern. Mounts `Titlebar`, `ToastViewport`, applies preferences sync via `lumotia:preferences-changed`, applies theme.

### `src/routes/viewer/+page.svelte` (606 LOC)

Transcript editor. Hydrates from a transcript ID handed off via `localStorage` (`lumotia:viewer-handoff` style key, see file). Fetches the full row from SQLite via `get_transcript`. Renders an `<audio>` element using `convertFileSrc()` to map the saved audio path to a webview URL. Provides per segment editing, debounced autosave through `saveTranscriptMeta`, virtual scrolling via `VirtualSegmentList`, playback speed control (`PLAYBACK_SPEEDS`), starring and tagging.

### `src/routes/preview/+layout@.svelte` (68 LOC)

Mounts only what the overlay needs: theme, preferences sync. No sidebar, no titlebar, no toast viewport.

### `src/routes/preview/+page.svelte` (274 LOC)

Live transcription overlay. Listens for `preview-listening`, `preview-cleanup`, `preview-hide` (and reads partial text via the same `Channel` pattern as Dictation, in some flows). State machine: `listening → live → cleanup → final → auto hide`. Provides a copy button (uses `navigator.clipboard.writeText` first, falls back to `copy_to_clipboard` invoke) and a revert to raw button. Auto hide timer constants live at the top of the file.

## Data flow

- Window creation: Rust opens four webviews and routes them to `/`, `/float`, `/viewer`, `/preview`. The same JS bundle loads in each.
- Cross window state:
  - `localStorage` carries `lumotia_settings` and the viewer handoff payload. Float window listens on the browser `storage` event to mirror settings.
  - Tauri events carry preferences (`lumotia:preferences-changed`), tray driven navigation (`lumotia:open-wind-down`), and float window focus (`task-window-focus`).
- Hotkey: only the main window registers, but every window's layout includes the migration `$effect`. The label guard at `+layout.svelte:115-121` prevents secondary windows from re registering.
- First run gating: only the main window evaluates and sets `page.current = "first-run"`. The float/viewer/preview windows skip the shell altogether.

## Watch outs

- The `useCustomChrome` flash is mitigated by defaulting to `false` on Linux. If you ever flip the default, expect a one frame flash of custom chrome before `loadOsInfo()` resolves.
- The break layouts duplicate the theme migration `$effect`. Touch one, touch all. Same for `applyExternalPreferences` listeners.
- `transcription-preview` is a borderless overlay with `WindowTypeHint = Utility`. Layout assumptions about chrome height do not apply.
- `+layout.svelte` calls `applyExternalPreferences` only after a payload `source` check. If you ever add a fifth window, its label must be propagated correctly or the window will echo its own preference writes.
- Drag drop on `FilesPage.svelte` listens on `tauri://drag-drop` events, which require the window to declare drag drop in `tauri.conf.json`. Slice 02 owns that surface.

## See also

- [Pages overview](pages-overview.md). What `page.current` ends up rendering.
- [Frontend ↔ Tauri bridge](frontend-tauri-bridge.md). Every command and event referenced above.
- [App shell and styling](app-shell-and-styling.md). The CSS plumbing that the shell relies on.
- [../02-tauri-runtime/README.md](../02-tauri-runtime/README.md). Window creation, tray, and the matching event surface.
