Room framework API
Room framework API
Why rooms exist
Rooms exist because different audio types need fundamentally different interaction models — not just different data filtered through the same UI. Music browsing (albums, artists, genres), podcast listening (subscriptions, episode progress, resume), audiobook reading (chapters, bookmarks, narration speed), and radio discovery (stations, now-playing, schedule) are genuinely different UX paradigms. A one-size-fits-all player (the same queue, the same metadata layout, the same navigation) forces every experience into a mold designed for none of them.
A room is not a page or a tab. A room is a bespoke interaction model for a type of audio experience. The shell provides the frame and navigation between rooms; the room owns everything inside that frame — navigation, metadata layout, search behavior, player expansion, context menus, sorting, filtering.
Room interface
interface Room {
id: string;
displayName: string;
icon: string; // icon identifier or path
// The shell renders this component
component: ComponentType<RoomProps>;
// Lifecycle hooks — optional
onActivate?(ctx: RoomContext): void;
onDeactivate?(): void;
// Search — optional. If not implemented, this room
// uses the default room-level search (all plugins, all types)
onSearch?(query: string): SearchHandler | void;
// Player expansion — optional. If not implemented,
// the expansion slot is empty (not hidden by CSS)
playerExpansion?: ComponentType<PlayerExpansionProps>;
}
interface RoomProps {
active: boolean;
ctx: RoomContext;
}
interface RoomContext {
search: (query: string) => Promise<SearchResults>;
queue: QueueHandle;
library: LibraryHandle;
playback: PlaybackHandle;
navigate: (view: string) => void;
}
ComponentType import
Room authors import ComponentType from the shell module, not from svelte directly:
// @rymflux/shell/types.ts — re-exports from svelte
export type { ComponentType } from 'svelte';
// Room authors:
import type { Room, ComponentType } from '@rymflux/shell/types';
When Web Component or WASM room support is added in v2, the re-export is swapped and room code does not change.
Player expansion slot
The shell reserves a named slot region. When the active room defines playerExpansion, the shell renders it into that slot when the player is in expanded state. Rooms that don’t define playerExpansion get nothing rendered in that region — the slot is empty, not hidden by CSS.
Expansion state is a shell-level boolean, toggled by the expand button, persisted in Svelte state (not SQLite).
Room discovery (v1)
All rooms are compiled into the Tauri binary. Registered at startup in a static array. v1 ships one room (Music Room). The Lobby/Home is a shell-level view, not a room.
// src/shell/rooms.ts
const ROOMS: Room[] = [
MusicRoom,
// Future experience rooms (Podcast, Audiobook, Radio, etc.) added here
];
No config-based discovery, no dynamic loading in v1.
Navigation: shell-level vs room-internal
Two distinct kinds of navigation exist:
-
Shell-level navigation — switching between rooms, or between a room and the Lobby. Owned by the shell. The shell renders a sidebar or tab bar; the user clicks to switch rooms. Rooms do not trigger this themselves.
RoomContext.navigate(view)is reserved for this purpose and is a no-op stub in v1 (only one room exists). -
Room-internal navigation — moving between sub-views within a room (e.g., Music Room: search results → album detail → track view → queue). Owned entirely by the room. The shell has no knowledge of room-internal views.
The expected pattern is a navigation stack managed by the room as local reactive state:
type MusicView = | { kind: 'search'; query: string } | { kind: 'album_detail'; album_id: string } | { kind: 'artist_detail'; artist_id: string } | { kind: 'queue' } | { kind: 'now_playing' } // Svelte 5 let viewStack = $state<MusicView[]>([{ kind: 'search', query: '' }]); let currentView = $derived(viewStack.at(-1)!); let canGoBack = $derived(viewStack.length > 1); function navigate(view: MusicView) { viewStack = [...viewStack, view]; } function goBack() { if (viewStack.length > 1) viewStack = viewStack.slice(0, -1); }No URL routing, no shell involvement. Rooms are not web pages — they are interaction models. A stack is simpler and the user gets back-navigation for free.
Room lifecycle (v1)
Inactive → shell activates → Active (onActivate called)
Active → shell deactivates → Inactive (onDeactivate called, component stays mounted)
No Suspended state in v1. Deactivated rooms remain fully mounted in the DOM — Svelte components aren’t destroyed, they’re hidden. This makes reactivation instant. If memory from keeping all rooms mounted becomes a problem, the fix is lazy mounting (don’t mount until first activation) rather than suspend/resume.
References
- ADR-009 (Room API TypeScript interface)
- FR-ROOM-1, FR-ROOM-2, FR-ROOM-3 (functional.md)