Widget System

The widget system is the foundation of our composable UI architecture, enabling dynamic application building through reusable, configurable components.

Overview

The widget system provides a powerful framework for building dynamic user interfaces through composable widgets. Each widget encapsulates specific functionality and can be configured, styled, and combined with other widgets to create complex applications.

graph TB
    subgraph "Widget Ecosystem"
        A[Widget Registry] --> B[Widget Definitions]
        B --> C[Widget Components]
        B --> D[Widget Schemas]
        B --> E[Widget Categories]
        
        F[Widget Tree] --> G[Root Widget]
        G --> H[Child Widgets]
        H --> I[Nested Widgets]
        
        J[Widget Renderer] --> K[Component Loader]
        K --> L[Dynamic Import]
        L --> M[Svelte Component]
        
        N[Widget Editor] --> O[Schema-based Forms]
        O --> P[Visual Configuration]
        P --> Q[Real-time Preview]
    end
    
    A --> F
    F --> J
    J --> N

Core Concepts

Widget Definition

A widget is defined by several key components:

interface WidgetRegistration {
    name: string;                         // Unique identifier
    title?: InlineTranslation;            // Localized display name
    description?: InlineTranslation;      // Localized widget description (required for catalog widgets)
    category?: string;                    // Organization category
    visibleInCatalog?: boolean;           // Show in widget picker
    componentLoader: () => Promise<any>;  // Dynamic component loading
    dataSchema?: () => Promise<any>;      // Configuration schema
}

Important: Widget descriptions use InlineTranslation (an object with language keys like { en: "...", de: "..." }), not plain strings. Descriptions are required for widgets with visibleInCatalog: true. They are displayed in the widget catalog and exposed via /docs/widgets/llm.txt for AI agents and chatbots.

Widget Instance

Each widget instance contains:

interface Widget {
    id: string;                      // Unique instance ID
    name: string;                    // Widget type from registry
    data?: any;                      // Widget configuration data
    styles?: WidgetCSSStyles;        // CSS styling
    slots?: WidgetSlotMapping;       // Child widget relationships
}

Widget Tree Structure

Widgets are organized in a hierarchical tree structure:

interface WidgetTree {
    rootWidgetId: string | undefined;     // Entry point
    widgets: Record<string, Widget>;      // All widget instances
}

Widget Categories

Layout Widgets

Widgets that provide structure and organization:

  • Container: Horizontal or vertical layout container
  • Max Width Container: Responsive width limiting
  • App Shell: Full application layout with header, sidebar, and content
  • Layout Child: Flexible layout positioning

Content Widgets

Widgets for displaying and editing content:

  • Text: Rich text display and editing
  • Image: Image display with responsive sizing
  • Editor.js: Block-style content editor
  • Hero: Large banner/hero sections
  • Check-in (CheckIn): Visitor check-in widget with registration form, optional QR code scanner, and GDPR-compliant data retention settings. See configuration details below.
  • Bunny Stream Video (BunnyStreamVideo): Embeds a video from Bunny Stream (bunny.net) with configurable playback options. See configuration details below.

Form Widgets

Interactive input and form components:

  • Form Fields: Various input types (text, number, date, etc.)
  • Form Containers: Form layout and validation
  • Button: Action triggers and navigation
  • Login: User authentication widget
  • Register: User registration widget

Data Widgets

Widgets for displaying and manipulating data:

  • DataView: Sortable, filterable data display with saved queries
  • EntityEditor: Edit entity records with validation
  • Dashboard: Customizable dashboard with multiple widgets
  • NumberStats: Display numeric statistics
  • NumberStatsDataTypeCount: Count statistics by data type
  • Todo: Task list and management
  • ProfileCard: User profile display
  • AddressCard: Address information display
  • StarRating: Rating input and display
  • Maps: Map display and location selection

Components for application navigation:

  • Navbar: Top-level navigation
  • Footer: Bottom navigation and links
  • Tabs: Tabbed content organization
  • Breadcrumbs: Hierarchical navigation

Special Purpose Widgets

Specialized widgets for specific use cases:

  • Kiosk: Full-screen kiosk mode for public displays
  • EmoticonsBar: Emoji picker and emoticon input
  • Error: Error display widget

EventStream Widgets

Real-time collaborative widgets for Event Streams:

  • EventStreamMap: GPS location sharing with OpenStreetMap integration (docs)
  • EventStreamImageGallery: Photo sharing with camera capture and gallery upload (docs)
  • EventStreamChatMessages: Real-time messaging for event streams (docs)
  • EventStreamTimeline: Chronological display of event entries (docs)
  • EventStreamPinBoard: Collaborative pin board for notes and ideas (docs)
  • EventStreamSettings: Configuration and settings for event streams (docs)

Website Building Block Widgets

Composable widgets for building public-facing website pages:

  • WebsiteSection: Configurable page section wrapper with variant (base, alternate, accent) and padding options (sm, md, lg, xl)
  • WebsiteNavigation: Fixed navigation bar with scroll-aware styling, responsive mobile menu, brand logo, navigation links, and CTA button
  • WebsiteFooter: Footer with contact information, navigation links, and social media icons
  • WebsiteFrame: Full-page wrapper combining WebsiteNavigation and WebsiteFooter around page content
  • CardGrid: Responsive card grid layout with configurable columns (1-3) and style variants (flat, elevated, gradient)
  • CtaSection: Call-to-action section with title, subtitle, button, and style variants (subtle, gradient)
  • FaqAccordion: Expandable FAQ section with question/answer pairs
  • FeatureList: Icon-based feature list for trust signals and feature highlights
  • PricingTable: Responsive pricing tier comparison table with feature checklists and CTA buttons

Utility Widgets

Specialized utility widgets:

  • QRCodeScanner: Real-time QR code and barcode scanning with configurable actions (docs)
  • IpForwarder: Network utility for IP forwarding and port mapping (docs)
  • PushNotificationSubscription: Browser notification subscription management

Bunny Stream Video Widget

Widget name constant: WIDGET_BUNNY_STREAM_VIDEO ("BunnyStreamVideo")
Category: content
Visible in catalog: yes

The Bunny Stream Video widget embeds a video hosted on Bunny Stream (bunny.net) using a responsive <iframe> with a standard 16:9 aspect ratio. Playback behaviour is fully configurable through widget properties that are appended as query parameters to the player URL.

How It Works

The widget takes a Bunny Stream player URL (e.g. https://player.mediadelivery.net/embed/...) and appends the configured playback options as query parameters before rendering it in a responsive <iframe>. If no URL is provided the widget renders nothing. If the URL cannot be parsed it is used as-is.

Configuration Properties

Property Type Default Description
url string "" Bunny Stream player URL (e.g. https://player.mediadelivery.net/embed/...). The widget renders nothing if this is empty.
autoplay boolean true Automatically starts playback when the page loads.
loop boolean true Loops the video continuously after it ends.
muted boolean true Mutes the video audio. Required by most browsers for autoplay to work.
preload boolean true Instructs the player to preload video data.
responsive boolean true Enables responsive sizing within the player.

Example Configuration

// Widget data object for the BunnyStreamVideo widget
const bunnyStreamVideoWidgetData = {
    url: "https://player.mediadelivery.net/embed/123456/abcdef-1234-5678-abcd-ef1234567890",
    autoplay: true,
    loop: true,
    muted: true,
    preload: true,
    responsive: true
};

Notes

  • The widget wraps the <iframe> in a position: relative; padding-top: 56.25% container to maintain a 16:9 aspect ratio at any container width.
  • The <iframe> has allow="accelerometer;gyroscope;autoplay;encrypted-media;picture-in-picture;" and allowfullscreen set by default.
  • The <iframe> uses loading="lazy" for deferred loading.
  • Because muted defaults to true, the autoplay default will work in all modern browsers without requiring user interaction.

Check-in Widget

Widget name constant: WIDGET_CHECKIN ("CheckIn")
Category: content
Visible in catalog: yes

The Check-in widget provides a self-service visitor registration kiosk that can be embedded on any page. It presents a registration form driven by a configurable visitor data type, optionally allows returning visitors to identify themselves via QR code scan, and enforces GDPR-compliant data retention by letting each visitor choose how long their data is stored.

How It Works

  1. Welcome screen — Displays the configured title and description with a prompt to register or scan a QR code.
  2. Registration form — Renders a schema-driven form based on the visitor data type. Internal fields (latestEventType, latestEventAt, deleteOn, deleteOnPreference) are hidden from the visitor and populated automatically.
  3. QR code scanner (optional) — Allows returning visitors to scan their visitor pass QR code to check out or update their record.
  4. Success screen — Shows a localized success message after check-in or check-out.
  5. Profile view — Allows the visitor to review or edit their registered data.

On submission the widget calls the server-side Visitors API (POST /api/projects/[projectId]/visitors) and records a corresponding visitor event (POST /api/projects/[projectId]/visitors/[visitorId]/events).

Configuration Properties

Property Type Required Description
title InlineTranslation No Heading shown on the welcome screen (e.g. { en: "Welcome", de: "Willkommen" }).
description InlineTranslation No Introductory text shown below the title on the welcome screen.
successSignInMessage InlineTranslation No Message displayed after a successful check-in.
successSignOutMessage InlineTranslation No Message displayed after a successful check-out.
smallPrint InlineTranslation No Privacy/legal notice shown at the bottom of the registration form (e.g. GDPR retention notice).
scannerEnabled boolean No When true, a QR code scanner button is shown so returning visitors can identify themselves by scanning their visitor pass. Defaults to false.
scannerButtonText InlineTranslation No Label for the QR code scanner button. Only shown when scannerEnabled is true.
deletionPeriods DeletionPeriod[] No List of retention period options presented to the visitor. Each entry has duration (milliseconds), label (InlineTranslation), and isDefault (boolean). The entry marked isDefault: true is pre-selected. If omitted, a default of 28 days (2 419 200 000 ms) is used.
visitorTypeId string No ID of the data type used to store visitor records. The widget derives the collection name and registration form schema from this type.
visitorEventTypeId string No ID of the data type used to store visitor events (check-in / check-out log entries).
contactsTypeId string No ID of the contacts data type, used to link a visitor to an internal contact person.

DeletionPeriod object

interface DeletionPeriod {
    duration: number;           // Retention duration in milliseconds
    label: InlineTranslation;   // Displayed option label
    isDefault: boolean;         // Pre-select this option
}

Example Configuration

// Widget data object for the CheckIn widget
const checkinWidgetData = {
    title: { en: "Welcome", de: "Willkommen" },
    description: {
        en: "Please register as a visitor",
        de: "Bitte registriere dich als Besucher"
    },
    successSignInMessage: {
        en: "You have been checked in successfully.",
        de: "Du wurdest erfolgreich eingecheckt."
    },
    successSignOutMessage: {
        en: "You have been checked out successfully.",
        de: "Du wurdest erfolgreich ausgecheckt."
    },
    smallPrint: {
        en: "Your data will be stored according to GDPR regulations and automatically deleted after the selected retention period.",
        de: "Deine Daten werden gemäß DSGVO gespeichert und nach dem ausgewählten Aufbewahrungszeitraum automatisch gelöscht."
    },
    scannerEnabled: true,
    scannerButtonText: { en: "Scan QR Code", de: "QR-Code scannen" },
    deletionPeriods: [
        {
            duration: 604800000,   // 7 days
            label: { en: "7 days", de: "7 Tage" },
            isDefault: false
        },
        {
            duration: 2419200000,  // 28 days
            label: { en: "28 days", de: "28 Tage" },
            isDefault: true
        },
        {
            duration: 7776000000,  // 90 days
            label: { en: "90 days", de: "90 Tage" },
            isDefault: false
        }
    ],
    visitorTypeId: "<visitor-type-id>",
    visitorEventTypeId: "<visitor-event-type-id>",
    contactsTypeId: "<contacts-type-id>"
};

Template Integration

The Check-in widget is the core UI component of the Check-in feature template (FeatureTemplateName.CHECKIN). When the template is applied the checkinAppExtension automatically creates an application containing a pre-configured CheckIn widget instance wired to the generated visitor and visitor-events data types. The template now also requires the CONTACT feature so that visitor records can reference an internal contact person.

Related extensions created by the template:

Extension Purpose
checkinAppExtension Creates the check-in application with a pre-configured CheckIn widget.
checkinGdprExtension Creates a nightly cron action that deletes visitor records whose deleteOn timestamp has passed.
checkinNotificationsExtension Creates email actions for visitor passes, contact-person notifications, and calendar invitations.
visitorFeatureTypeExtension Creates the visitor data type (name, email, company, contact person, check-in status fields).
visitorEventsFeatureTypeExtension Creates the visitor events data type (sign-in / sign-out log).
visitorFeatureBOMExtension Adds a visitors management page to the backoffice navigation.
visitorEventsFeatureBOMExtension Adds a visitor events log page to the backoffice navigation.

Server-Side API

The widget communicates with the following project-scoped API endpoints:

Method Path Description
POST /api/projects/[projectId]/visitors Creates a new visitor record. Body: { collectionName, data }. Returns { id } with status 201.
GET /api/projects/[projectId]/visitors/[visitorId] Retrieves a visitor record. Query param: collectionName.
PATCH /api/projects/[projectId]/visitors/[visitorId] Updates a visitor record. Body: { collectionName, data }.
DELETE /api/projects/[projectId]/visitors/[visitorId] Deletes a visitor record. Query param: collectionName.
POST /api/projects/[projectId]/visitors/[visitorId]/events Creates a visitor event (check-in or check-out). Body: { collectionName, data }. Returns { id } with status 201.

Widget Registration

Widgets are registered in the central registry:

// Widget registration example
registerWidget({
    name: WIDGET_TEXT,
    title: { de: "Text" },
    description: { en: "A simple text display widget for showing static or dynamic text content with configurable formatting and support for Text Replacer syntax." },
    visibleInCatalog: true,
    category: "content",
    componentLoader: () => import("../../widgets/text/TextWidget.svelte"),
    dataSchema: () => import("../../widgets/text/TextWidget.schema.json")
});

Registration Properties

  • name: Unique widget identifier (constant)
  • title: Localized display name
  • description: Localized widget description as InlineTranslation object (required for catalog widgets)
  • category: Organization category for the widget picker
  • visibleInCatalog: Whether to show in the visual editor
  • componentLoader: Dynamic component import function
  • dataSchema: Configuration schema for the widget editor

Description Requirement

Description is required for all widgets with visibleInCatalog: true. This ensures:

  1. AI agents and chatbots can accurately describe widget capabilities via /docs/widgets/llm.txt
  2. Users can discover widget features in the widget catalog with markdown rendering
  3. Developers have concise reference material during integration

The description should include:

  • Overview of the widget's purpose
  • Key features and capabilities
  • Important configuration details
  • Configuration properties table
  • Use cases
  • Technical implementation details (where relevant)

Accessing Widget Documentation

import { 
    getWidgetDescription, 
    getAllWidgetDocumentation 
} from '@smallstack/client';

// Get description for a specific widget
const description = getWidgetDescription('EventStreamMap');

// Get all widget documentation (for documentation tools/chatbots)
const allDocs = await getAllWidgetDocumentation();

Widget documentation is also available via LLM-friendly endpoints:

  • Main platform docs: /llm.txt - Links to widget catalog
  • Widget catalog: /docs/widgets/llm.txt - Complete widget documentation for AI agents

Widget Schemas

Each widget can define a JSON schema for its configuration:

{
    "type": "object",
    "properties": {
        "text": {
            "type": "string",
            "title": "Text Content",
            "description": "The text to display"
        },
        "fontSize": {
            "type": "string",
            "title": "Font Size",
            "enum": ["small", "medium", "large"],
            "default": "medium"
        },
        "color": {
            "type": "string",
            "title": "Text Color",
            "x-type-schema": {
                "inputWidgetConfiguration": {
                    "string": { "type": "color" }
                }
            }
        }
    }
}

Schema Features

  • Type Validation: Ensures data integrity
  • UI Generation: Automatically generates configuration forms
  • Localization: Supports multiple languages
  • Custom Input Types: Specialized input widgets
  • Validation Rules: Client and server-side validation

Widget Lifecycle

sequenceDiagram
    participant R as Registry
    participant L as Loader
    participant C as Component
    participant E as Editor
    
    R->>L: Register Widget
    L->>L: Store Definition
    E->>L: Request Widget
    L->>C: Dynamic Import
    C->>L: Component Instance
    L->>E: Rendered Widget
    E->>C: Configuration Updates
    C->>C: Re-render

Lifecycle Stages

  1. Registration: Widget is registered in the central registry
  2. Discovery: Widget becomes available in the widget picker
  3. Selection: User selects widget for their application
  4. Configuration: User configures widget properties
  5. Rendering: Widget is rendered in the application
  6. Updates: Configuration changes trigger re-rendering
  7. Persistence: Widget state is saved to the database

Widget Slots

Widgets can contain other widgets through a slot system:

interface WidgetSlotMapping {
    [slotName: string]: string[];  // Maps slot names to widget IDs
}

Slot Examples

// Container widget with multiple slots
const containerWidget: Widget = {
    id: "container-1",
    name: "Container",
    slots: {
        "children": ["text-1", "image-1", "button-1"]
    }
};

// App shell with named slots
const appShellWidget: Widget = {
    id: "app-shell-1",
    name: "AppShell",
    slots: {
        "header": ["navbar-1"],
        "sidebar": ["navigation-1"],
        "content": ["main-content-1"],
        "footer": ["footer-1"]
    }
};

Widget Styling

Widgets support CSS styling through the styles property:

interface WidgetCSSStyles {
    [key: string]: string;
}

const styledWidget: Widget = {
    id: "text-1",
    name: "Text",
    styles: {
        "color": "#333333",
        "fontSize": "18px",
        "fontWeight": "bold",
        "padding": "16px",
        "margin": "8px 0"
    }
};

Styling Features

  • CSS Properties: Direct CSS property mapping
  • Responsive Design: Media query support
  • Theme Integration: Automatic theme variable usage
  • Style Inheritance: Parent-child style relationships

Creating Custom Widgets

1. Create the Svelte Component

<!-- CustomWidget.svelte -->
<script lang="ts">
    interface Props {
        title?: string;
        content?: string;
        showIcon?: boolean;
    }
    
    const { 
        title = 'Default Title',
        content = 'Default content',
        showIcon = false
    }: Props = $props();
</script>

<div class="custom-widget">
    {#if showIcon}
        <i class="fas fa-star"></i>
    {/if}
    <h3>{title}</h3>
    <p>{content}</p>
</div>

2. Define the Schema

{
    "type": "object",
    "properties": {
        "title": {
            "type": "string",
            "title": "Widget Title",
            "default": "My Custom Widget"
        },
        "content": {
            "type": "string",
            "title": "Content",
            "x-type-schema": {
                "inputWidget": "textarea"
            }
        },
        "showIcon": {
            "type": "boolean",
            "title": "Show Icon",
            "default": false
        }
    }
}

3. Register the Widget

// In widget-registry.ts
registerWidget({
    name: "CustomWidget",
    title: { en: "Custom Widget", de: "Benutzerdefiniertes Widget" },
    description: { 
        en: "A custom widget example",
        de: "Ein Beispiel für ein benutzerdefiniertes Widget"
    },
    category: "custom",
    visibleInCatalog: true,
    componentLoader: () => import("./CustomWidget.svelte"),
    dataSchema: () => import("./CustomWidget.schema.json")
});

Widget Testing

Unit Testing

import { render } from '@testing-library/svelte';
import { describe, it, expect } from 'vitest';
import CustomWidget from './CustomWidget.svelte';

describe('CustomWidget', () => {
    it('renders with default props', () => {
        const { getByText } = render(CustomWidget);
        expect(getByText('Default Title')).toBeInTheDocument();
    });
    
    it('renders with custom data', () => {
        const data = { title: 'Test Title', content: 'Test Content' };
        const { getByText } = render(CustomWidget, { data });
        expect(getByText('Test Title')).toBeInTheDocument();
        expect(getByText('Test Content')).toBeInTheDocument();
    });
});

Integration Testing

import { test, expect } from '@playwright/test';

test('widget can be added and configured', async ({ page }) => {
    await page.goto('/editor');
    
    // Add widget to page
    await page.click('[data-testid="widget-picker"]');
    await page.click('[data-testid="custom-widget"]');
    
    // Configure widget
    await page.fill('[data-testid="title-input"]', 'Test Title');
    await page.fill('[data-testid="content-input"]', 'Test Content');
    
    // Verify widget is rendered
    await expect(page.locator('[data-testid="custom-widget"]')).toContainText('Test Title');
});

Best Practices

Widget Design

  1. Single Responsibility: Each widget should have one clear purpose
  2. Configurability: Expose relevant configuration options
  3. Accessibility: Follow WCAG guidelines for all interactive elements
  4. Performance: Optimize for fast loading and rendering
  5. Mobile-First: Design for mobile devices first

Schema Design

  1. Clear Naming: Use descriptive property names
  2. Validation: Include appropriate validation rules
  3. Defaults: Provide sensible default values
  4. Documentation: Include descriptions for all properties
  5. Localization: Support multiple languages where appropriate

Component Architecture

  1. Props Interface: Define clear TypeScript interfaces
  2. Reactive Updates: Use Svelte's reactivity system effectively
  3. Error Handling: Handle invalid props gracefully
  4. Style Isolation: Avoid global CSS conflicts
  5. Event Handling: Emit events for parent communication

Troubleshooting

Common Issues

  1. Widget Not Loading: Check dynamic import paths and component exports
  2. Schema Validation Errors: Verify JSON schema syntax and property types
  3. Styling Conflicts: Use CSS modules or scoped styles
  4. Performance Issues: Optimize component re-rendering and data loading
  5. Registration Errors: Ensure unique widget names and proper registration syntax

Debugging Tools

  1. Widget Inspector: Browser extension for widget tree visualization
  2. Schema Validator: Online tool for schema validation
  3. Component Profiler: Performance analysis for widget rendering
  4. Network Monitor: Track widget loading and data fetching
  5. Console Logging: Strategic logging for debugging widget behavior

Next Steps