@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.

v0.29.34Svelte 5RunesBrowser AIModuleUIRegistry

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-ai install
  • Preload strategies + warm cache: none/eager/idle/on-visible, plus a module-level Map that survives SvelteKit navigation and component remounts
  • ModuleUIRegistry: cross-package Svelte component discovery; consumer packages register slots via side-effect imports of their /svelte subpath
  • Permission system: PermissionCheck component and use:permission action
  • Dual theme system: simple ThemeProvider with design tokens, plus full preset system (material/glass/studio) with CSS generation and runtime switching
  • Ripple action: material-style use:ripple for tactile feedback

Installation

bash
pnpm add @happyvertical/smrt-svelte

Peer dependencies (all optional): svelte >=5.18.2, @happyvertical/smrt-agents, @happyvertical/smrt-jobs, @happyvertical/smrt-profiles, @happyvertical/smrt-users.

Quick Start

Provider Setup

svelte
<!-- +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

svelte
<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

HookReturns
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 PathContents
@happyvertical/smrt-svelteProvider, DataTable, permission utilities, hooks, state, core components
@happyvertical/smrt-svelte/adminAgentAdminPanel, AgentAdminTabs, AgentSettingsShell
@happyvertical/smrt-svelte/calendarCalendar, DayView
@happyvertical/smrt-svelte/formsTextInput, Select, MoneyInput, DateTimeInput, Toggle, FileUpload, AddressInput, and more
@happyvertical/smrt-svelte/layoutContainer, Grid, Header, Footer, Masthead, PageHeader, EmptyState, SummaryCard
@happyvertical/smrt-svelte/uiButton, Card, Badge, Pagination
@happyvertical/smrt-svelte/themesThemeProvider, presets (material/glass/studio), CSS generation
@happyvertical/smrt-svelte/registryModuleUIRegistry β€” the cross-package component registry
@happyvertical/smrt-svelte/workspaceWorkspaceShell, RoleShell, NavTree, Breadcrumbs, navTreeFromManifest, ToolsDock + defineToolsDock
@happyvertical/smrt-svelte/workspace/servercomposeDockAvailability β€” server-side tool gate evaluation
@happyvertical/smrt-svelte/i18nClient i18n: useI18n, Trans, defineMessages (languages-free)
@happyvertical/smrt-svelte/i18n/serverbuildI18nSnapshot β€” Node-only server snapshot builder
@happyvertical/smrt-svelte/browser-aiBundled browser AI client (STT/TTS/LLM adapters, capability detection)
@happyvertical/smrt-svelte/browser-ai/svelteSvelte AI components (VoiceInput, CapabilityGate, DownloadProgress, etc.)
@happyvertical/smrt-svelte/styles/tokens.cssDesign 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.

json
{
  "exports": {
    "./svelte": {
      "types": "./dist/svelte/index.d.ts",
      "import": "./dist/svelte/index.js"
    }
  }
}

API Surface

MethodPurpose
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)

typescript
// 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

typescript
// 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

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.)

svelte
<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 the extends chain, 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 your PermissionResolver at the +layout.server.ts level. 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; sectionHints maps a qualifier substring to a section title; basePath (default /api/v1) prefixes hrefs.
typescript
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).

typescript
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 like chat.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 with t(key, vars) and a reactive locale. Safe to call outside a Provider (falls back to registered English defaults). <Trans> is the component form.
typescript
// +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 };
}
svelte
<!-- +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 use
  • eager β€” load immediately on mount
  • idle β€” load when browser is idle (recommended)
  • on-visible β€” load when component enters viewport

Adapters

CapabilityAdapters
STTbrowser-speech (Web Speech API), whisper-cpp, whisper-wasm
TTSbrowser-synthesis (SpeechSynthesis API)
LLMwebllm, transformers-llm

Warm Cache API

typescript
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().

svelte
<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

svelte
<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/: simple ThemeProvider with design tokens β€” drop-in for apps that only need light/dark + a couple of overrides
  • src/themes/: full preset system with material, glass, and studio presets, CSS generation at build, and runtime switching via ThemeSwitcher
svelte
<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 initialisation
  • src/state/ β€” SmrtAppStateManager ($state rune), warm client cache
  • src/hooks/ β€” useAuth, useSocket, useAppState, useSTT, useTTS, useLLM, useTheme
  • src/components/ β€” UI components by category
  • src/themes/ β€” ThemeProvider, ThemeSwitcher, CSS presets
  • src/browser-ai/ β€” STT/TTS/LLM adapters, capability detection (bundled, not external)
  • src/registry/ β€” ModuleUIRegistry for cross-package component discovery
  • src/components/workspace/ β€” WorkspaceShell, RoleShell, NavTree, navTreeFromManifest, tools dock
  • src/i18n/ β€” client i18n (useI18n, Trans) + server.ts snapshot builder