@happyvertical/smrt-svelte
Svelte 5 component library for SMRT: Provider, hooks, browser AI (STT/TTS/LLM with warm cache), dual theme system, permission-aware rendering, and the ModuleUIRegistry for cross-package component discovery.
Overview
@happyvertical/smrt-svelte is the canonical UI package for the SMRT
framework. It provides a root Provider component, seven hooks, 56+ Svelte 5
components organized into subpath exports, bundled browser AI adapters with a warm client
cache that survives navigation, a dual theme system, permission actions, and the ModuleUIRegistry β the cross-package registry that other SMRT UI shippers (smrt-content,
smrt-images, smrt-assets, smrt-analytics, smrt-chat, smrt-jobs, smrt-agents) plug into at import
time.
Key Capabilities
- Provider: root component for
+layout.svelteβ initialises auth state, permissions, WebSocket, AI capabilities, and the warm client cache - 7 Hooks:
useAuth,useSocket,useAppState,useSTT,useTTS,useLLM,useTheme - Bundled Browser AI: STT (browser-speech, whisper-cpp, whisper-wasm), TTS
(browser-synthesis), LLM (webllm, transformers-llm). All adapters live inside this package
now β no separate
browser-aiinstall - Preload strategies + warm cache:
none/eager/idle/on-visible, plus a module-levelMapthat survives SvelteKit navigation and component remounts - ModuleUIRegistry: cross-package Svelte component discovery; consumer
packages register slots via side-effect imports of their
/sveltesubpath - Permission system:
PermissionCheckcomponent anduse:permissionaction - Dual theme system: simple
ThemeProviderwith design tokens, plus full preset system (material/glass/studio) with CSS generation and runtime switching - Ripple action: material-style
use:ripplefor tactile feedback
Installation
pnpm add @happyvertical/smrt-sveltePeer dependencies (all optional): svelte >=5.18.2, @happyvertical/smrt-agents, @happyvertical/smrt-jobs, @happyvertical/smrt-profiles, @happyvertical/smrt-users.
Quick Start
Provider Setup
<!-- +layout.svelte -->
<script>
import { Provider } from '@happyvertical/smrt-svelte';
let { data, children } = $props();
</script>
<Provider user={data.user} permissions={data.permissions}
ai={{ preload: 'idle', stt: { type: 'whisper-cpp' } }}>
{@render children()}
</Provider>Using Hooks
<script>
import { useAuth, useSTT } from '@happyvertical/smrt-svelte';
const { user, isAuthenticated, hasPermission } = useAuth();
const { start, stop, isListening, lastResult } = useSTT();
</script>
{#if isAuthenticated}
<p>Hello, {user.displayName}</p>
<button onclick={() => isListening ? stop() : start()}>
{isListening ? 'Stop' : 'Listen'}
</button>
<p>{lastResult}</p>
{/if}Hooks
| Hook | Returns |
|---|---|
useAuth() | user, isAuthenticated, permissions, hasPermission() |
useSocket() | status, isConnected, send(), reconnect(), disconnect() |
useAppState() | Full SmrtAppStateManager β mode, AI adapters, capabilities |
useSTT() | start(), stop(), isListening, lastResult, interimResult |
useTTS() | speak(), stop(), isSpeaking, getVoices() |
useLLM() | chat(), initialize(), unload(), isGenerating, downloadProgress |
useTheme() | Theme context from ThemeProvider |
Subpath Exports
The package uses subpath exports to organise components by category. Import from the specific subpath for tree-shaking.
| Import Path | Contents |
|---|---|
@happyvertical/smrt-svelte | Provider, DataTable, permission utilities, hooks, state, core components |
@happyvertical/smrt-svelte/admin | AgentAdminPanel, AgentAdminTabs, AgentSettingsShell |
@happyvertical/smrt-svelte/calendar | Calendar, DayView |
@happyvertical/smrt-svelte/forms | TextInput, Select, MoneyInput, DateTimeInput, Toggle, FileUpload, AddressInput, and more |
@happyvertical/smrt-svelte/layout | Container, Grid, Header, Footer, Masthead, PageHeader, EmptyState, SummaryCard |
@happyvertical/smrt-svelte/ui | Button, Card, Badge, Pagination |
@happyvertical/smrt-svelte/themes | ThemeProvider, presets (material/glass/studio), CSS generation |
@happyvertical/smrt-svelte/registry | ModuleUIRegistry β the cross-package component registry |
@happyvertical/smrt-svelte/workspace | WorkspaceShell, RoleShell, NavTree, Breadcrumbs, navTreeFromManifest, ToolsDock + defineToolsDock |
@happyvertical/smrt-svelte/workspace/server | composeDockAvailability β server-side tool gate evaluation |
@happyvertical/smrt-svelte/i18n | Client i18n: useI18n, Trans, defineMessages (languages-free) |
@happyvertical/smrt-svelte/i18n/server | buildI18nSnapshot β Node-only server snapshot builder |
@happyvertical/smrt-svelte/browser-ai | Bundled browser AI client (STT/TTS/LLM adapters, capability detection) |
@happyvertical/smrt-svelte/browser-ai/svelte | Svelte AI components (VoiceInput, CapabilityGate, DownloadProgress, etc.) |
@happyvertical/smrt-svelte/styles/tokens.css | Design tokens CSS |
ModuleUIRegistry
ModuleUIRegistry is the global singleton that lets SMRT UI-shipping packages
advertise Svelte components keyed by (moduleName, slotId). Host applications discover
components by name without hard-coding imports; consumer packages register their slots via
side-effect imports of their /svelte subpath.
Subpath Contract (PR #1213)
Every UI-shipping package exposes a ./svelte conditional export with only types and import keys β no svelte condition. Vite's pre-bundler resolves the subpath
like any other ESM entry, which avoids the dual package hazards we hit when both svelte and import conditions resolved to different files.
{
"exports": {
"./svelte": {
"types": "./dist/svelte/index.d.ts",
"import": "./dist/svelte/index.js"
}
}
}API Surface
| Method | Purpose |
|---|---|
registerModule(meta) | Register a module's SmrtModuleMeta (name, version, slots) |
register(moduleName, slotId, component) | Register a Svelte component for a slot |
get(moduleName, slotId) | Look up a component; returns undefined if missing |
has(moduleName, slotId) | Existence check |
getSlots(moduleName) | List slot IDs for a module |
getModules() | List registered module names |
Consumer Registration (smrt-commerce example)
// packages/commerce/src/svelte/index.ts
import { ModuleUIRegistry } from '@happyvertical/smrt-svelte/registry';
import { COMMERCE_MODULE_META } from '../ui.js';
import InvoiceCard from './components/InvoiceCard.svelte';
import InvoiceHeader from './components/InvoiceHeader.svelte';
import InvoiceLineItems from './components/InvoiceLineItems.svelte';
// Side-effect: register module metadata + slots at import time
ModuleUIRegistry.registerModule(COMMERCE_MODULE_META);
ModuleUIRegistry.register('@happyvertical/smrt-commerce', 'invoice-card', InvoiceCard);
ModuleUIRegistry.register('@happyvertical/smrt-commerce', 'invoice-header', InvoiceHeader);
ModuleUIRegistry.register('@happyvertical/smrt-commerce', 'invoice-line-items', InvoiceLineItems);
// Also re-export for direct imports
export { InvoiceCard, InvoiceHeader, InvoiceLineItems };Host Lookup
// SvelteKit host app
import { ModuleUIRegistry } from '@happyvertical/smrt-svelte/registry';
import '@happyvertical/smrt-commerce/svelte'; // side-effect import registers slots
import '@happyvertical/smrt-content/svelte';
import '@happyvertical/smrt-images/svelte';
const InvoiceCard = ModuleUIRegistry.get('@happyvertical/smrt-commerce', 'invoice-card');
// Render <InvoiceCard ... /> if present, fall back to placeholder otherwise.Packages that register slots
smrt-content
Article/Document cards, thumbnails
smrt-images
Image editing + categorization slots
smrt-assets
Asset list/picker slots
smrt-analytics
Report viewers
smrt-chat
Room + thread slots
smrt-jobs
Task/schedule dashboard slots
smrt-agents
Agent admin slots
See also smrt-playground, the discovery host that renders every registered slot for live exploration.
Workspace Shell Primitives
The @happyvertical/smrt-svelte/workspace subpath ships SvelteKit-agnostic, SSR-safe
admin-shell primitives: a layout shell, a multi-role wrapper, nav primitives, and a right-rail
tools dock. They carry no domain coupling and consume --smrt-color-* tokens
directly β drop them into any Svelte 5 app.
WorkspaceShell & RoleShell
WorkspaceShell is a composition of snippet slots (brand, nav, sidebarFooter, topbarActions, inspectorRail, β¦) that lays out a sidebar + sticky topbar + main content + optional
inspector panel. It is responsive (sidebar collapses to an icon rail on tablet, becomes a drawer
on mobile) and owns no domain logic. RoleShell is an opinionated thin wrapper over
it: pass a list of RoleConfigs and the active role id, and it wires that role's nav sections into the sidebar and breadcrumb trail. An optional color on the
role is exposed as a --smrt-role-color custom property for theming. (Missing-role
behaviour: throws in dev, degrades to the first/empty role in production.)
<script lang="ts">
import { RoleShell } from '@happyvertical/smrt-svelte/workspace';
import type { RoleConfig } from '@happyvertical/smrt-svelte/workspace';
import { page } from '$app/state';
const roles: RoleConfig[] = [
{
id: 'admin',
label: 'Admin',
color: 'blue',
sections: [
{ label: 'Content', children: [
{ href: '/api/v1/articles', label: 'Articles' },
] },
],
},
];
</script>
<RoleShell {roles} currentRole="admin" currentPath={page.url.pathname}>
{#snippet topbarActions()}<AccountMenu />{/snippet}
</RoleShell>navTreeFromManifest β manifest-driven nav
Rather than hand-writing nav config for dozens of @smrt() classes, navTreeFromManifest(manifest, options) is a pure (data-in/data-out) helper that
turns a SMRT manifest into the NavSection[] shape NavTree and RoleConfig.sections consume. It filters out collection classes, visibility: 'internal' | 'test' entries, and objects with no REST list route, then groups the rest into sections (alphabetically sorted for deterministic output).
- STI ancestor-walk dedup β STI subtypes that share an ancestor's
collection(and thus the same polymorphic REST URL) are suppressed by walking theextendschain, so you get one nav link per route instead of duplicates. - Decoupled from RBAC β pass
permittedResources(a plain array of qualified class names) to filter per role; resolve the allow-list with yourPermissionResolverat the+layout.server.tslevel. The helper also expands the allow-list up STI parents so a permitted subtype keeps its shared-route link. - Labels/icons/sections β
@smrt({ ui: { label, icon } })drives the item label/icon;sectionHintsmaps a qualifier substring to a section title;basePath(default/api/v1) prefixes hrefs.
import { manifest } from '$lib/manifest';
import { navTreeFromManifest } from '@happyvertical/smrt-svelte/workspace';
const sections = navTreeFromManifest(manifest, {
sectionHints: {
'@happyvertical/smrt-content': 'Content',
'@happyvertical/smrt-commerce': 'Commerce',
},
permittedResources: editorAllowList, // optional per-role filter
});
// β feed straight into RoleConfig.sections or <NavTree items={sections} />ToolsDock & defineToolsDock
defineToolsDock(options) creates a reactive right-rail dock registry; the ToolsDock component renders it, and useToolsDock() reads the instance
from context. Each ToolDef has an arbitrary string id, a label, an icon (glyph or iconComponent), and a panel component rendered when active. The dock exposes a small reactive API
(open()/close()/toggle(), setContext(), refreshAvailability(), typed on()/emit() events) and the
context blob ({ type, title, url, data, actions }) is generically typed so
tool components get typed context.data / context.actions without a cast.
Tools can declare gates (e.g. 'permission:articles.publish', 'feature:video-tools') evaluated server-side via composeDockAvailability from @happyvertical/smrt-svelte/workspace/server.
Each gate's prefix selects a caller-supplied evaluator; the framework ships no built-in evaluators
and throws on an unknown prefix (loud-fail beats silent-leak).
import { defineToolsDock } from '@happyvertical/smrt-svelte/workspace';
import ChatPanel from './ChatPanel.svelte';
import MessageSquare from 'lucide-svelte/icons/message-square';
const dock = defineToolsDock({
tools: [
{
id: 'chat',
label: 'Chat',
iconComponent: MessageSquare,
component: ChatPanel,
gates: ['permission:chat.use'], // evaluated server-side
},
],
});
dock.setContext({ type: 'route', data: { siteSlug: 'demo' } });
dock.open('chat');Internationalization (i18n)
smrt-svelte ships a two-layer i18n system split across two subpaths so the heavy languages
resolver never reaches the client bundle. The browser layer
(@happyvertical/smrt-svelte/i18n) is languages-free; the server bridge
(@happyvertical/smrt-svelte/i18n/server) pulls smrt-languages and is Node-only.
defineMessages()β a package registers its English code-default templates (keys likechat.message_input.placeholder).buildI18nSnapshot({ locale, tenantId?, db?, keys? })β server-side, resolves a{ locale, messages }snapshot by walking each key through the code β app/tenant override β locale chain. Variables are not applied server-side; the client interpolates with the same renderer.useI18n()β returns the reactive store witht(key, vars)and a reactivelocale. Safe to call outside aProvider(falls back to registered English defaults).<Trans>is the component form.
// +layout.server.ts -- resolve the snapshot for the request locale
import { buildI18nSnapshot } from '@happyvertical/smrt-svelte/i18n/server';
export async function load({ locals }) {
const i18n = await buildI18nSnapshot({
locale: locals.locale ?? 'en',
tenantId: locals.tenantId,
db: locals.db, // omit to resolve code defaults only
});
return { i18n };
}<!-- +layout.svelte -- hand the snapshot to Provider -->
<script>
import { Provider } from '@happyvertical/smrt-svelte';
let { data, children } = $props();
</script>
<Provider i18n={data.i18n}>{@render children()}</Provider>
<!-- any component -->
<script lang="ts">
import { useI18n } from '@happyvertical/smrt-svelte/i18n';
const { t } = useI18n();
</script>
<input placeholder={t('chat.message_input.placeholder')} />Browser AI
The browser AI system (previously the separate browser-ai package, consolidated
in v0.20) is bundled inside smrt-svelte and provides on-device speech-to-text, text-to-speech,
and LLM inference. A module-level warm client cache (a singleton Map) survives
navigation and remounts, avoiding repeated WASM and AI model downloads.
Preload Strategies
noneβ load on first useeagerβ load immediately on mountidleβ load when browser is idle (recommended)on-visibleβ load when component enters viewport
Adapters
| Capability | Adapters |
|---|---|
| STT | browser-speech (Web Speech API), whisper-cpp, whisper-wasm |
| TTS | browser-synthesis (SpeechSynthesis API) |
| LLM | webllm, transformers-llm |
Warm Cache API
import {
getCachedSTT,
getCachedTTS,
getCachedLLM,
getCacheStats,
clearAllCaches,
} from '@happyvertical/smrt-svelte';
// Check what's loaded
const stats = getCacheStats();
console.log(stats);
// { stt: { count, types }, tts: { count, types }, llm: { count, keys } }
// Get a cached adapter (returns undefined if not loaded);
// pass the adapter type (LLM also takes an optional modelId)
const stt = getCachedSTT('whisper-cpp');Streaming chat with useLLM
useLLM() runs an LLM fully on-device. Streaming is surfaced through the onToken callback on chat(text, { systemPrompt?, onToken? }) β
tokens arrive incrementally and the same full string is also returned when generation finishes.
The hook exposes reactive isInitializing, isGenerating, downloadProgress (a percent), isReady, currentModel, and error, plus initialize() / unload().
<script lang="ts">
import { useLLM } from '@happyvertical/smrt-svelte';
const llm = useLLM({ systemPrompt: 'You are a helpful assistant.' });
let input = $state('');
let response = $state('');
async function send() {
response = '';
// onToken streams partial output; the resolved value is the full text.
await llm.chat(input, {
onToken: (token) => { response += token; },
});
}
</script>
{#if llm.isInitializing}
<p>Loading model⦠{llm.downloadProgress}%</p>
{:else}
<form onsubmit={send}>
<input bind:value={input} />
<button disabled={llm.isGenerating}>Send</button>
</form>
<p>{response}</p>
{/if}Permission-Aware Rendering
<script>
import { PermissionCheck, permission } from '@happyvertical/smrt-svelte';
</script>
<!-- Component-based -->
<PermissionCheck requires="admin:write">
<button>Admin Action</button>
</PermissionCheck>
<!-- Action-based (disables element by default) -->
<div use:permission={{ slug: 'articles.delete', permissions: userPermissions }}>
Delete
</div>
<!-- Hide-only variant (removes from layout if not permitted) -->
<div use:permission={{ slug: 'articles.delete', permissions: userPermissions, hideOnly: true }}>
Delete
</div>Theme System
Two layered theme systems for different levels of customisation:
src/theme/: simpleThemeProviderwith design tokens β drop-in for apps that only need light/dark + a couple of overridessrc/themes/: full preset system withmaterial,glass, andstudiopresets, CSS generation at build, and runtime switching viaThemeSwitcher
<script>
import { ThemeProvider, ThemeSwitcher } from '@happyvertical/smrt-svelte/themes';
</script>
<ThemeProvider preset="glass" colorScheme="system">
<ThemeSwitcher />
{@render children()}
</ThemeProvider>Key Files
src/Provider.svelteβ root component, state initialisationsrc/state/βSmrtAppStateManager($state rune), warm client cachesrc/hooks/βuseAuth,useSocket,useAppState,useSTT,useTTS,useLLM,useThemesrc/components/β UI components by categorysrc/themes/βThemeProvider,ThemeSwitcher, CSS presetssrc/browser-ai/β STT/TTS/LLM adapters, capability detection (bundled, not external)src/registry/βModuleUIRegistryfor cross-package component discoverysrc/components/workspace/βWorkspaceShell,RoleShell,NavTree,navTreeFromManifest, tools docksrc/i18n/β client i18n (useI18n,Trans) +server.tssnapshot builder