UI Widgets
UI Widgets are custom widgets that can be added to UI Pages. Widgets can display data, provide controls, show charts, integrate external services, or present any custom visualization.
Overview
The UI Widgets page provides two ways to build a custom widget:
- Designer widgets — built visually from composed elements (text, live values, buttons, zone controls, cameras, images) with no code. Best for status tiles, button panels, and branded layouts. See Widget Designer.
- Code widgets — custom Svelte 5 components with full programmatic control, compiled server-side and delivered to clients as standalone components. See Code Editor.
Both kinds appear in the same widget list, are placeable on UI Pages the same way, and keep automatic revision history.
Widget Designer
Click New Designer to create a widget visually. A designer widget is a tree of elements you compose and configure with forms — no CSS or JavaScript involved. The definition is stored as data on the widget record, so it never needs compiling and always renders with the site's active theme.
Elements
| Element | Purpose |
|---|---|
| Section | A row or column that holds other elements (gap, alignment, wrap, optional themed background panel) |
| Text | A heading or caption (size, weight, theme color role, alignment) |
| Live Value | A self-updating attribute readout from a zone or device (caption, icon, unit suffix, decimals) |
| Button | Runs a zone/device command, a macro, or opens a page — with an optional image face, a live state badge bound to an attribute, and a separate press-and-hold action |
| Slider | A level slider bound to a zone or device attribute (brightness, volume, shade position) that sends a command when moved |
| Zone Control | Embeds the full inline control for a zone (dimmer, thermostat, shade, …) |
| Camera | A live camera view |
| Image | A logo or picture, optionally tappable |
| Spacer | Flexible or fixed empty space |
Working in the designer
- The left pane shows the structure list (the element tree) and the property form for the selected element. Property forms use the same pickers as the rest of the admin — zones, devices, and macros are selected by name, never by raw ID.
- The live preview renders the widget exactly as a UI page will, with the current theme applied. Click any element in the preview to select it — the property form follows. While designing, element interactions are suppressed (clicking a button selects it instead of firing its action).
- Buttons in the toolbar add elements (into the selected section, or next to the selected element), and the property header has move up/down, indent/outdent, and delete.
- Drag rows in the structure list to restructure: drop on a section's middle to move the element into it, or on any row's top/bottom edge to place it before/after that row. A section can't be dropped inside itself. The indent/outdent buttons do the same moves without dragging (handy on tablets) — indent moves the element into the nearest section above it, outdent pulls it out of its current section.
- A warning strip below the structure list flags incomplete elements (e.g. a Zone Control without a zone).
- The panels are resizable — drag the divider between the structure list and the property form, or between the editor and the live preview. Double-click a divider to reset it; the sizes are remembered per browser.
- Save persists the element tree. Unsaved edits survive switching between widgets and even a page reload (Draft indicator); they're kept until saved, or discarded by restoring a revision.
Layout is flex-based: sections flow their children as rows or columns, and each element has Grow / Align Self / Margin settings under its Layout group. There is no pixel positioning — widgets stay responsive across screen sizes, orientations, and themes.
Interface Layout
The page is split into sections:
Left Side - Widget List:
- Dropdown selector of all custom widgets (code and designer kinds; designer widgets are marked "DESIGNER")
- Filter/search capabilities
- New Designer / New Code buttons
- History button (revision history)
- Delete widget button
- Recompile All button
Right Side - Widget Editor:
- Widget configuration form
- Code editor with syntax highlighting
- Live preview pane
- Error display
- Save/test buttons
Creating a Widget
To create a new widget:
- Click New Designer (visual, no code) or New Code (Svelte source)
- Enter widget title when prompted (e.g., "Weather Dashboard")
- Widget is created with a starter layout (designer) or starter code
- Customize the widget in the designer or code editor
- Click Save (code widgets compile on save; designer widgets save instantly)
AI Assistant
The code editor includes an AI Assistant sidebar (the AI button in the editor) that can write, modify, and explain widget code, and applies changes through the same build/compile pipeline as a manual save. Type a request and press Enter, or paste images directly into the chat input — a screenshot of a design to recreate, a mockup, or a photo of the current widget's rendering problem. Pasted images appear as thumbnails above the input; click ✕ to remove one before sending. Up to 5 images per message; large images are automatically downscaled before upload.
When more than one AI provider (or more than one model) is configured under the assistant's providers editor, a provider/model picker appears at the top of the sidebar. The selection applies per request and is remembered per browser; it's independent of the admin assistant drawer's own picker.
Revision History
Every save of a custom widget automatically snapshots a revision (the last 20 are kept per widget). Click History to list them — each entry shows the time, what triggered it (create, save, or restore), and the content size. Restore returns the widget to that revision; the state you're leaving is itself kept in history, so a restore never loses anything. Code widgets recompile automatically after a restore. Revisions are captured no matter how the widget was edited — the admin editor, the AI assistant, or the REST API.
Widget Configuration
Basic Settings
Widget Selector
- Select existing widget to edit
- Shows all custom widgets — code and designer kinds (designer widgets marked "DESIGNER")
- Switching widgets preserves unsaved changes — a Draft indicator appears next to the selector when the open widget differs from its saved state. Drafts (code and designer) also survive a page reload; saving the widget clears its draft
Name
- Internal identifier (lowercase_with_underscores)
- Auto-generated from title
- Cannot contain spaces or special characters
Title
- Display name shown in widget selector
- Appears in UI page configuration
- User-friendly text
Enabled
- Toggle to enable/disable widget
- Disabled widgets:
- Don't compile
- Cannot be added to pages
- Configuration preserved
Admin Only
- When on, the widget is hidden from the end-user UI Pages widget picker
- Used for admin dashboard tiles (Monitor Tag rollups, Active Zones, Climate Issues, etc.) that don't make sense on regular user-facing UIs
- Admin-only widgets are still addable to the admin home dashboard, which is itself a UI page (
admin_dashboard) — edit it from UI Pages (the dashboard's Customize button deep-links there) - An ADMIN badge appears next to the Title field while editing an admin-only widget, and the widget selector dropdown appends
• ADMINafter the name so the scope is visible at a glance
Widget size on a page is not set here — pages are bento grids, so a widget occupies grid cells sized by the Column Span / Row Span set per placement in UI Pages → Widget Configuration.
Code Editor
Full-featured Svelte 5 code editor:
Features
- Syntax Highlighting: JavaScript, HTML, CSS
- Line Numbers: Easy navigation
- Auto-Save: Preserves work automatically (draft saved to browser)
- Tab Support: Proper code indentation
- Find/Replace: Standard editor features
- Full Screen: Expand editor for focused coding
Widget Structure
Widgets follow standard Svelte single-file component format:
<script>
import {onMount} from 'svelte';
import GemApp from '../gem/app';
let data = [];
onMount(async function() {
// Initialize widget
loadData();
});
async function loadData() {
// Fetch data for widget
data = await GemApp.getInstance().query('zone', {subsystem_id: 1});
}
</script>
<style>
.widget-container {
padding: 15px;
height: 100%;
}
.widget-title {
font-size: 1.2rem;
font-weight: 600;
margin-bottom: 10px;
}
</style>
<div class="widget-container">
<div class="widget-title">My Widget</div>
{#each data as item}
<div>{item.label}: {item.power}</div>
{/each}
</div>
Available APIs in Widgets
Widgets have access to:
GemApp Instance:
const gem = GemApp.getInstance();
Common Methods:
// Query data
let zones = await gem.query('zone', {subsystem_id: 1});
let device = await gem.queryOne('device', {id: 5});
// Send commands
await gem.command({zone_id: 10, command: 'on'});
// Execute macros
await gem.macro(macroId);
// Get attributes
let attrs = await gem.getAttributes('zone', zoneId);
let value = await gem.getAttribute('device', deviceId, 'temperature');
// Set attributes
await gem.setAttribute('zone', zoneId, 'brightness', 75, 'int');
// Subscribe to updates
let token = await gem.subscribe('zone', zoneId, (data) => {
console.log('Zone updated:', data);
});
// Unsubscribe
await gem.unsubscribe('zone', zoneId, token);
// Notifications
gem.showMessage({message: 'Action complete', severity: 'success'});
Svelte 5 Features:
<script>
import {onMount} from 'svelte';
let count = $state(0); // Reactive state
let doubled = $derived(count * 2); // Derived value
function increment() {
count++;
}
</script>
<button onclick={increment}>
Count: {count} (Doubled: {doubled})
</button>
Live Preview
The preview pane shows the widget as it will appear:
Preview Features
- Live Rendering: Widget renders in real-time
- Theme Applied: Uses current system theme, including its Layout & Presentation flags (state-dot rectangle buttons, scene chips)
- Interactive: Widget functionality works in preview
- Error Display: Compilation/runtime errors shown below
Preview Controls
Collapse/Expand
- Toggle preview visibility
- Collapse for more editor space
- Expand to see changes
Preview Settings
Click Settings in the Live Preview header (next to Hide) to open the container layout options a page would apply — without changing the saved widget. The preview renders full-size; adjust the settings and the widget re-renders live so you can judge how it will sit on a real page:
- Show Container — draw the panel chrome (title bar, background, border) or render transparently
- Auto-size — scale the widget contents to fit the container
- CSS Class / Inline Style — extra class / inline CSS on the container
- Content Style (
inner_style) — inline CSS on the inner content area (e.g.justify-content: flex-startto top/left-align) - Transition — CSS transition applied to the container
These settings are the same catalog applied when a widget is placed on a page (see UI Pages → Container Config). They affect the preview only and are not saved to the widget.
Compiling Widgets
Automatic Compilation
When you click Save:
- Widget configuration saved to database
- Widget code compiled to JavaScript
- Compilation output cached
- Preview updated automatically
- Success or error message shown
Compilation Process
Steps:
- Svelte compiler processes the code
- JavaScript generated
- CSS extracted and scoped
- Component registered with GEM
- Cache updated
- UIs notified of new widget version
Compilation Errors
If compilation fails:
Error Display:
- Red error box appears below preview
- Shows error message
- Shows line number and column
- Indicates problem area
Common Errors:
- Syntax errors (missing brackets, quotes)
- Invalid Svelte syntax
- Import errors
- Type errors
Fix Process:
- Review error message
- Locate problem line
- Fix the issue
- Click Save to recompile
- Error clears when successful
Runtime Errors
Errors that occur when widget runs:
Error Display:
- Shown in preview below widget
- Logged to browser console
- May include stack trace
Common Causes:
- API call failures
- Null reference errors
- Invalid data format
- Subscription errors
Debugging:
- Use
console.log()extensively - Check browser developer console
- Verify API calls return expected data
- Test with sample data first
Recompile All Widgets
The Recompile All button recompiles all enabled widgets:
When to Use:
- After Svelte Upgrade: Ensure compatibility with new Svelte version
- After System Update: Recompile for new GEM APIs
- Bulk Fix: Apply compilation improvements to all widgets
- Cache Clear: Force regeneration of all widget code
Process:
- Click Recompile All
- Confirm action
- System compiles all enabled widgets
- Success/failure message shows results
- Individual errors logged for failed widgets
Common Widget Types
Status Widgets
Display system or device status:
<script>
import {onMount} from 'svelte';
let devices = $state([]);
onMount(async () => {
devices = await GemApp.getInstance().query('device', {enabled: true});
});
</script>
<div class="status-widget">
<h3>Device Status</h3>
{#each devices as device}
<div class:online={device.connected} class:offline={!device.connected}>
{device.label}: {device.connected ? 'Online' : 'Offline'}
</div>
{/each}
</div>
Chart Widgets
Display data visualizations:
<script>
import {onMount} from 'svelte';
import Chart from 'chart.js/auto';
let canvas;
let history = $state([]);
onMount(async () => {
history = await GemApp.getInstance().query('attribute_history', {
name: 'temperature',
_limit: 100
});
new Chart(canvas, {
type: 'line',
data: {
labels: history.map(h => h.timestamp),
datasets: [{
label: 'Temperature',
data: history.map(h => h.value)
}]
}
});
});
</script>
<canvas bind:this={canvas}></canvas>
Control Widgets
Interactive controls:
<script>
let zones = $state([]);
async function toggleZone(zoneId) {
await GemApp.getInstance().command({
zone_id: zoneId,
command: 'toggle'
});
await loadZones(); // Refresh
}
</script>
<div class="controls">
{#each zones as zone}
<button onclick={() => toggleZone(zone.id)}>
{zone.label}: {zone.power}
</button>
{/each}
</div>
Data Widgets
Display API or sensor data:
<script>
import {onMount} from 'svelte';
let weather = $state(null);
onMount(async () => {
// Get weather from attribute or API
let w = await GemApp.getInstance().getAttribute('system', 0, 'weather_data');
weather = JSON.parse(w.weather_data || '{}');
});
</script>
{#if weather}
<div class="weather-widget">
<div class="temp">{weather.temp}°F</div>
<div class="conditions">{weather.conditions}</div>
</div>
{/if}
Camera Widgets
Display camera feeds:
<script>
export let cameraUrl = 'rtsp://camera.local/stream';
// Proxy camera stream through GEM
let streamUrl = `/api/camera/stream?url=${encodeURIComponent(cameraUrl)}`;
</script>
<div class="camera-widget">
<img src={streamUrl} alt="Camera Feed" />
</div>
Advanced Topics
Widget Configuration Schema
Widgets can accept configuration:
// In widget code
export let config = {};
// Access config values
let zoneId = config.zone_id;
let refreshRate = config.refresh_rate || 5000;
Configure when adding widget to page:
{
"zone_id": 10,
"refresh_rate": 3000,
"show_graph": true
}
Real-Time Updates
Subscribe to real-time zone/device updates:
<script>
import {onMount, onDestroy} from 'svelte';
let zone = $state(null);
let token;
onMount(async () => {
const zoneId = 10;
// Initial load
zone = await GemApp.getInstance().queryOne('zone', {id: zoneId});
// Subscribe to updates
token = await GemApp.getInstance().subscribe('zone', zoneId, (updatedZone) => {
zone = updatedZone; // Auto-updates UI
});
});
onDestroy(async () => {
if (token) {
await GemApp.getInstance().unsubscribe('zone', 10, token);
}
});
</script>
<div>
Zone: {zone?.label}
Power: {zone?.power}
</div>
Built-in Components
Widgets can reuse GEM's own building-block components through gem.components,
so you don't have to rebuild buttons, sliders, or live readouts by hand:
<script>
const Button = gem.components.Button;
const BoundValue = gem.components.BoundValue;
</script>
<BoundValue systemTarget="zone" targetID={7} name="temperature" suffix="°" />
<Button text="On" on:press={() => gem.command({zone_id: 7, command: 'on'})} />
Available under gem.components: Button, ImageButton, PowerButton,
PresetButton, ButtonList, ZoneMapButton, Link, Keyboard, Keypad,
Slider, LightDimmer, LightSwitch, ChannelButton, ChannelBrowser,
ZoneMap, CameraZoneViewer, Bifold, Modal, ZoneControlModal, and
BoundValue.
BoundValue is a self-subscribing, always-current readout of a single attribute — it subscribes on mount, renders the live value, and cleans up on destroy, so it replaces the manual subscribe/unsubscribe boilerplate above:
| Prop | Purpose |
|---|---|
systemTarget | Entity type — zone, device, … |
targetID | Numeric entity id |
name | Attribute name, e.g. temperature, state, level |
format | Optional (value) => value transform |
suffix | Optional unit appended after the value, e.g. °, % |
placeholder | Shown while loading or when the value is empty (default —) |
Inline Zone Controls
gem.zoneControls maps each zone.control style name to its control component,
letting you drop a full zone control directly into a widget without opening the
modal:
<svelte:component this={gem.zoneControls[zone.control]} {zone} />
This is the same component set used by the Zone Control modal, so the rendered
control matches what a user sees elsewhere. Keys include light_dimmer,
light_switch, light_spectrum, shade_dimmer, shade_manual,
climate_default, climate_setpoint, climate_dual_setpoint, fan_default,
gate_default, door_lock, power_default, and more — one per zone Control
type (see Zones).
Third-Party Libraries
Import and use external libraries:
<script>
import Chart from 'chart.js/auto';
import dayjs from 'dayjs';
// Other npm packages available in GEM
</script>
Available Libraries (already installed in GEM):
- chart.js
- dayjs
- lodash
- And many more (check package.json)
State Management
Complex widgets can use Svelte stores:
<script>
import {writable} from 'svelte/store';
const zones = writable([]);
async function loadZones() {
let data = await GemApp.getInstance().query('zone', {subsystem_id: 1});
zones.set(data);
}
onMount(loadZones);
</script>
{#each $zones as zone}
<div>{zone.label}</div>
{/each}
Widget Best Practices
-
Error Handling: Always handle API failures gracefully
-
Loading States: Show loading indicators
-
Null Checks: Verify data exists before accessing
-
Cleanup: Unsubscribe in onDestroy
-
Performance: Avoid excessive polling or subscriptions
-
Responsive: Design for multiple screen sizes
-
Theme Compatibility: Use theme CSS variables
-
Documentation: Comment complex logic
-
Testing: Test in preview before deploying
-
Version Control: Export widget code to external files
Troubleshooting
Widget Won't Compile
Check:
- Syntax Errors: Review error message for line number
- Missing Imports: Verify all imports are available
- Svelte Version: Ensure Svelte 5 syntax
- Quotes: Use consistent quote style
- Brackets: Ensure all brackets are closed
Widget Crashes on Load
Check:
- API Calls: Wrap in try/catch
- Null Values: Check for null before accessing properties
- Subscriptions: Ensure valid IDs
- Browser Console: Check for detailed error messages
Widget Shows Blank
Check:
- Data Loading: Verify API calls return data
- Conditional Rendering: Check {#if} conditions
- CSS: Verify styles don't hide content
- Size: Widget has reasonable width/height
- Console: Check for errors
Preview Not Updating
Try:
- Click Save to recompile
- Toggle preview collapse/expand
- Clear browser cache
- Check for compilation errors
Cannot Save Widget
Check:
- Name: Must be valid (lowercase_with_underscores)
- Compilation: Widget must compile successfully
- Permissions: User has permission to update widgets
- Network: Connection to GEM server