Skip to main content

@happyvertical/smrt-svelte

Board

@happyvertical/smrt-svelte/board provides a generic, accessible Svelte 5 Kanban-style board. It has no project, sales, or support dependency: supply your own cards, columns, card-to-column getter/setter, and typed card snippet. cards is controlled; defaultCards enables local state. onmove receives a typed intent and can be async—rejections restore the previous view. Set optimistic to present controlled moves while persistence is pending. Set allowSameColumnReorder={false} when an adapter persists only lane/status transitions rather than a position within a lane.

<script lang="ts">
import { Board } from '@happyvertical/smrt-svelte/board';
</script>

<Board
{columns}
{cards}
getCardColumnId={(card) => card.stage}
setCardColumnId={(card, stage) => ({ ...card, stage })}
getCardLabel={(card) => card.subject}
card={cardSnippet}
onmove={({ card, target }) => save({ ...card, stage: target.columnId })}
/>

The same primitive can represent support queues (card.queue) or a sales pipeline (opportunity.stage) without importing @happyvertical/smrt-projects.

Svelte 5 component library for the s-m-r-t framework. Provides UI components, browser AI integration (STT/TTS/LLM with warm cache), a theme system, permission-aware rendering, and module UI registry for agent admin panels.

Installation

pnpm add @happyvertical/smrt-svelte

Data-surface browser bridge security

The browser bridge is a transport adapter, not an authentication system. Configure it only with a session/source binding established by the server and use a transport that supplies verified peer metadata. It accepts commands only from the configured server peer and emits acknowledgements/events only on the bound route; wire sessionId and source fields must never be treated as proof of identity.

The adapter canonicalizes requests and applies the shared identifier limit from @happyvertical/smrt-ui/data-surface along with bounded envelopes before calling the registry. The registry remains the authority for command authorization and execution. Command IDs are idempotent while their bounded replay entries are retained; concurrent same-signature requests coalesce, a conflicting signature is rejected, and replay-capacity exhaustion is reported explicitly. Malformed requests and unauthenticated peers are ignored before an acknowledgement; valid requests that expire or encounter disconnect and transport failures produce bounded protocol outcomes without exposing registry state.

Usage

Query-backed data surfaces

Use the web binding when a table should render one remote page instead of hydrating its whole collection. It exposes rows, page, total, loading, refreshing, stale, error, retry, lastUpdated, and the latest applied result. The result getter also follows query-scoped live replacements, so consumers can keep envelope metadata in sync with the rows.

<script lang="ts">
import { remoteQuery } from '@happyvertical/smrt-svelte/web';
const view = remoteQuery(collection, transport);
// Failures remain available as view.error for reactive rendering.
void view.execute(request).catch(() => undefined);
</script>

{#if view.loading}<p>Loading…</p>{/if}
{#each view.rows as row (row.id)}<div>{row.name}</div>{/each}

Provider Setup

<script>
import { Provider } from '@happyvertical/smrt-svelte';
let { children } = $props();
</script>

<Provider user={data.user} permissions={data.permissions}
ai={{ preload: 'idle', stt: { type: 'whisper-cpp' } }}>
{@render children()}
</Provider>

The Provider can own generated WebMCP tools for the same lifecycle. Its policy is identical to registerWebMcpTools; omitted effects exposes reads only:

<Provider webmcp={{
definitions,
effects: ['read', 'write'],
namespace: 'workspace',
maxTools: 24
}}>
{@render children()}
</Provider>

This controls capability exposure, not authorization. Tool execution still crosses the authenticated REST boundary and must retain its auth and tenancy guards. The effects, filter, and filterTool policy applies to generated data/model tools only; the fixed mounted-UI adapter has the separate controls described below.

WebMCP test doubles and polyfills should implement the browser's promise-returning document.modelContext.registerTool() contract; declare the function async when migrating older void-returning fixtures.

Mounted UI through WebMCP

<Provider webmcp> registers six fixed smrt_ui_* tools for the mounted UI: list, inspect, and execute for form controls and data surfaces. The tool set does not change as components mount and unmount; each call reads the current transport-neutral registries instead of inspecting or simulating the DOM.

Forms automatically join the Provider's control registry. An explicit Form interactionRegistry still takes precedence. Pass the same data-surface registry used by DataTable or CollectionToolbar when those mounted surfaces should be discoverable:

<script lang="ts">
import { createDataSurfaceRegistry } from '@happyvertical/smrt-ui/data';
import { Provider } from '@happyvertical/smrt-svelte';

const surfaces = createDataSurfaceRegistry();
</script>

<Provider webmcp={{ ui: { dataSurfaceRegistry: surfaces } }}>
<!-- pass {surfaces} to mounted data-surface components -->
{@render children()}
</Provider>

A page with its own hand-rolled list markup — not DataTable — that already mirrors a headless DataTableController's search/filters/sort/page/selection can register that same registry in one call with mountListDataSurface (@happyvertical/smrt-svelte/web) instead of hand-writing the registration:

import { createDataTableController } from '@happyvertical/smrt-ui/data';
import { mountListDataSurface } from '@happyvertical/smrt-svelte/web';

const controller = createDataTableController();
const handle = mountListDataSurface({
registry: surfaces,
descriptor: myListDescriptor,
controller,
context: { totalRows, queryFingerprint },
refresh: () => reload(),
});

// Only `controller` is observed automatically. App-owned `context` is a
// one-time snapshot at mount — publish a fresh one whenever totalRows,
// queryFingerprint, or freshness changes (an $effect keyed on those values
// is the usual place):
handle.update({ totalRows, queryFingerprint });

// on unmount:
handle.destroy();

It mirrors controller into the registry, translates visible table commands back into controller.dispatch() calls, and routes the fixed refresh/retry/focus/reveal/highlight controls to callbacks; any other controlId goes through an onControl escape hatch (denied by default) — except a canonical table-control id (set-filters, reset, set-page, … the full DATA_TABLE_SURFACE_CONTROL_IDS list from @happyvertical/smrt-ui/data), which is always intercepted first and never reaches onControl, even under a custom label. See docs/data-surface-conformance.md for the full contract.

The default prefix is smrt_ui_. Configure ui.prefix when multiple Providers must coexist in one document; the same prefix cannot be registered twice. The six derived names are reserved through the document-global tool-name lock, so a generated model tool, a view intent, or a useWebMcpTool tool that would take one of them under your prefix fails at registration with a message naming the tool and its current owner, rather than being silently dropped by the browser. ui: false disables only the fixed UI adapter while leaving generated model tools enabled. For compatibility, an object config that omits ui continues to enable only generated model tools; use webmcp={true} or provide ui: {} to enable the mounted-UI adapter.

Form commands always run with source: 'agent'. WebMCP input cannot assert confirmation: staging is allowed by the registry policy, while apply, clear, and undo require a separate human-confirmed path. Secret control values and hidden data-surface columns are not serialized. Read responses are marked as untrusted content. Bespoke useWebMcpTool and <Form webmcp> tools retain their existing lifecycle and submit behavior.

Custom rich fields may continue to call registerField(field) and later unregisterField(name). New code should retain and invoke the disposer returned by registerField: it is bound to that exact registration, so cleanup cannot remove a same-name replacement. The return value is additive; legacy form contexts whose registerField returns void remain supported. Context accessors bind legacy name-based cleanup to registrations made by that caller, so overlapping same-name fields can unmount in either order without retaining a detached control.

Form Components

<script>
import { TextInput, Select, MoneyInput, DateTimeInput, Toggle } from '@happyvertical/smrt-svelte/forms';
</script>

<TextInput label="Name" bind:value={name} />
<MoneyInput label="Price" bind:value={price} currency="USD" />
<DateTimeInput label="Launch Date" bind:value={date} />
<Toggle label="Active" bind:checked={active} />

UI Foundation

<script>
import { Button, Card, Badge, Pagination } from '@happyvertical/smrt-ui/ui';
import { DataTable } from '@happyvertical/smrt-ui/data';
</script>

<Card>
<DataTable columns={cols} data={rows} pageSize={20} />
<Pagination currentPage={1} totalPages={5} />
</Card>

Permission-Aware Rendering

<script>
import { PermissionCheck, permission } from '@happyvertical/smrt-ui';
</script>

<PermissionCheck requires="admin:write">
<button>Admin Action</button>
</PermissionCheck>

<!-- Or as a Svelte action -->
<div use:permission={{ slug: 'admin:read', permissions: userPermissions }}>
Protected content
</div>

Theme System

<script>
import { ThemeProvider } from '@happyvertical/smrt-ui/themes';
</script>

<ThemeProvider preset="glass" colorScheme="system">
{@render children()}
</ThemeProvider>

Admin Workspace

<script lang="ts">
import { manifest } from '$lib/smrt-manifest';
import {
AdminShell,
TenantNav,
tenantNavFromManifest,
} from '@happyvertical/smrt-svelte/workspace';

let { children } = $props();

const sections = tenantNavFromManifest(manifest, {
sectionHints: {
'@happyvertical/smrt-content': 'Content',
'@happyvertical/smrt-profiles': 'Profiles',
},
});

</script>

<AdminShell title="Admin">
{#snippet tenantPanel()}
<TenantNav items={sections} currentHref="/admin/articles" />
{/snippet}

{@render children?.()}
</AdminShell>

Filter the same manifest by role permissions when only a subset of resources should be visible:

import { tenantNavFromManifest } from '@happyvertical/smrt-svelte/workspace';

const editorSections = tenantNavFromManifest(manifest, {
permittedResources: [
'@happyvertical/smrt-content:Article',
'@happyvertical/smrt-content:Document',
],
sectionHints: {
'@happyvertical/smrt-content': 'Content',
},
});

In SvelteKit, build the nav in a +layout.server.ts (server-side, no client fetch) and mount AdminShell in +layout.svelte. The template-sveltekit scaffold adopts AdminShell as its default chrome exactly this way; copy its src/routes/+layout.server.ts / +layout.svelte / settings/+page.svelte.

  • Migration guide (first-generation WorkspaceShell/RoleShellAdminShell; adoption is additive and non-breaking): src/components/workspace/MIGRATION.md
  • Playground demos: playground/src/routes/admin-shell exercises all four scopes, focus tools, and activities; admin-shell-activity-feed and admin-shell-system-feed show live feeds.

Exports

Entry Points

This is the complete exports map of this package. Anything not listed is not importable, even if it appears in dist/.

Import PathContents
@happyvertical/smrt-svelteProvider, hooks (useAppState, useAuth, useLLM, useSocket, useSTT, useTheme, useTTS), app state/context, ModulePanel, and the form components below
@happyvertical/smrt-svelte/formsForm inputs (TextInput, Select, MoneyInput, DateTimeInput, Toggle, etc.)
@happyvertical/smrt-svelte/settingsServer-paged settings search, selection, and list/detail layout (SettingsCatalog, paginateSettingsCatalog)
@happyvertical/smrt-svelte/workspaceAdminShell, ShellState, tenant nav, focus tools, settings, activities, and system/app panels
@happyvertical/smrt-svelte/workspace/legacyOpt-in ToolsDock compatibility surface for applications migrating to AdminShell
@happyvertical/smrt-svelte/workspace/serverServer-side workspace helpers (Node only)
@happyvertical/smrt-svelte/workspace/livesystemFeed — the AdminShell system scope (jobs/schedules/dispatch) polled from an app status endpoint; deliberately carries no smrt-web dependency
@happyvertical/smrt-svelte/browser-aiBrowser AI client (STT/TTS/LLM adapters, capability detection)
@happyvertical/smrt-svelte/browser-ai/svelteSvelte AI components (VoiceInput, CapabilityGate, etc.)
@happyvertical/smrt-svelte/websmrt-web live-query bindings (liveCollection, activityFeed, useUpdateAvailable) plus mountListDataSurface (custom-list data-surface registration)
@happyvertical/smrt-svelte/i18n/serverServer-side i18n resolver (Node only)

Domain-agnostic UI lives in @happyvertical/smrt-ui. There is no ui, layout, calendar, data, chat, feedback, registry, themes, i18n, or styles/tokens.css subpath on smrt-svelte, so those specifiers only resolve against smrt-ui:

Import PathContents
@happyvertical/smrt-uiPermissionCheck, permission / hasPermission / hasAnyPermission / hasAllPermissions
@happyvertical/smrt-ui/uiUI primitives (Button, Card, Badge, Pagination)
@happyvertical/smrt-ui/layoutLayout (Container, Grid, Header, Footer, Masthead, etc.)
@happyvertical/smrt-ui/calendarCalendar and DayView
@happyvertical/smrt-ui/dataDataTable, CollectionList/ContentList, CollectionToolbar
@happyvertical/smrt-ui/feedbackModal, ConfirmDialog, LoadingOverlay, ProgressBar
@happyvertical/smrt-ui/chatMessage bubble, reaction picker, typing indicator
@happyvertical/smrt-ui/registryModuleUIRegistry for agent admin panels
@happyvertical/smrt-ui/themesCanonical ThemeProvider, Material/Glass/Studio/s-m-r-t/HappyVertical presets, CSS generation
@happyvertical/smrt-ui/i18nClient i18n (useI18n, Trans) — the counterpart to smrt-svelte's /i18n/server
@happyvertical/smrt-ui/styles/tokens.cssDesign tokens CSS

forms is the one name on both: @happyvertical/smrt-ui/forms holds the Provider-free primitives (Input, Select, Textarea, Toggle, FormGroup), and @happyvertical/smrt-svelte/forms re-exports those and adds the Provider-backed inputs, so it stays the one-stop barrel for applications.

The first-generation WorkspaceShell, RoleShell, NavTree, and Breadcrumbs have no entry point at all. Their .svelte files are copied into dist/ but no export subpath or barrel names them, so they cannot be imported from an installed package — see the migration guide. AdminShell supersedes them.

Legacy ToolsDock availability is presentation-only, not authorization. fetchAvailability failures intentionally keep controls usable using the current context's last-known-good result, or registered-tool metadata after a context change. Every tool operation and server endpoint must independently enforce permissions. Consumers can surface current-context degraded state through dock.availabilityError; a context change or later valid refresh clears it.

Components by Category

Forms: AddressInput, CheckboxInput, DateRangeInput, DateTimeInput, FileUpload, Form, FormGroup, FormMicButton, Input, MeasurementInput, MoneyInput, NumberInput, PhoneInput, SearchInput, Select, SelectInput, Textarea, TextareaInput, TextInput, Toggle

Layout: Container, EmptyState, Footer, Grid, Header, Masthead, PageHeader, SummaryCard

UI: Badge, Button, Card, Pagination

Display (from @happyvertical/smrt-ui): ConfidenceBadge, CurrencyDisplay, DateDisplay, Icon, StatusBadge

CurrencyDisplay accepts Commerce-compatible string currency fields. It trims and uppercases ISO 4217 codes before formatting, defaults to CAD, and renders an accessible inline error for malformed or unsupported codes instead of throwing and interrupting the surrounding collection render. The historical unit="cents" option means ISO minor units, so currencies with zero or three minor digits are scaled correctly. Minor-unit amounts must be finite safe integers; fractional or unsafe numeric values render an accessible inline error. unit="dollars" means major units. ISO codes whose minor unit is N.A. require unit="dollars"; minor-unit mode renders an accessible inline error for those codes, while major-unit mode uses a stable two-digit display policy. CAD and USD retain their symbol display; all other currencies render their ISO code for deterministic SSR hydration.

Feedback: ConfirmDialog, LoadingOverlay, Modal, ProgressBar

Navigation: FilterChips, Tabs

Data: DataTable

Permissions: PermissionCheck, RoleBadge, RoleSelector

Other: Calendar, DayView, MembershipCard, MembershipList, ModulePanel

The agent-admin shells (AgentAdminPanel, AgentAdminTabs, AgentSettingsShell) moved to @happyvertical/smrt-agents/svelte (#1589).

Browser AI: AILoadingOverlay, CapabilityGate, DownloadProgress, STTTest, VoiceInput

Hooks

useAuth, useSocket, useAppState, useSTT, useTTS, useLLM, useTheme

Functions & Actions

hasPermission, hasAnyPermission, hasAllPermissions, permission (action), ripple (action)

Cache API

getCachedSTT, getCachedTTS, getCachedLLM, getCacheStats, clearAllCaches

Dependencies

  • @happyvertical/smrt-types -- shared type definitions
  • Peer: svelte >=5.18.2, @happyvertical/smrt-jobs, @happyvertical/smrt-profiles, @happyvertical/smrt-users (all optional)