Overview

Plugins

Widgets

Reference

Publishing

Desktop Widgets

Desktop widgets are web components injected into the Hiclaro dashboard as a self-contained JavaScript bundle.

Bundle contract

The desktop client fetches widgets/desktop/bundle.js, injects it into the page as a <script> tag, and then waits for each declared tag to be registered via customElements.define. The bundle must satisfy the following constraints:

  • Self-contained IIFE. The bundle must be a single immediately-invoked function expression that calls customElements.define(tag, class extends HTMLElement {…}) for every widget it ships. No external imports, no dynamic import(), no lazy chunks, no CDN dependencies.

  • DOM properties, not attributes. widgetApi, instanceConfig, and pluginId are set on the element as JavaScript properties after it is appended to the DOM. They are complex objects or strings — do not try to read them as HTML attributes. React to them via set accessors.pluginId is a string containing the UUID of the plugin that owns this widget, injected by the host so that widgets can filter getAllAssets() without hardcoding an install-time identifier.

  • Inline all CSS. External stylesheets are not loaded. Use a Shadow DOM (this.attachShadow({ mode: 'open' })) with a <style> tag inside, or inject a CSSStyleSheet via the Constructable Stylesheets API. Shadow DOM also isolates your widget's styles from the host page.

  • Inline images and fonts. External asset URLs are not reachable in the Electron context. Encode small assets as data URIs, or fetch them at runtime via widgetApi if they must be served from your plugin's backend.

  • Allow app:// in CORS. In the Electron desktop app the renderer origin is app:// rather than http://localhost. If your widget makes direct HTTP calls to your plugin's own service backend, that backend must include app:// in its CORS allowed origins.

  • Clean up in disconnectedCallback. Unsubscribe from widgetApi.subscribeTelemetry and clear any timers or event listeners when the element is removed from the DOM.

Vanilla JS example

A minimal sensor widget using plain JavaScript and Shadow DOM. Build this with any bundler (esbuild, rollup, webpack) targeting a single IIFE output file.

(function () {
  class MySensorWidget extends HTMLElement {
    connectedCallback() {
      const shadow = this.attachShadow({ mode: 'open' });
      shadow.innerHTML = `
        <style>
          :host { display: block; padding: 16px; box-sizing: border-box; }
          #label { font-size: 0.875rem; opacity: 0.6; margin-bottom: 4px; }
          #value { font-size: 2rem; font-weight: 700; }
        </style>
        <div id="label"></div>
        <div id="value">—</div>
      `;
    }

    set widgetApi(api) {
      this._api = api;
      this._subscribe();
    }

    set instanceConfig(config) {
      this._config = config;
      const label = this.shadowRoot?.getElementById('label');
      if (label) label.textContent = config?.title ?? '';
      if (this._api) this._subscribe();
    }

    _subscribe() {
      const datasourceId = this._config?.datasourceId;
      if (!datasourceId || !this._api) return;
      this._unsubscribe?.();
      this._unsubscribe = this._api.subscribeTelemetry({
        datasourceIds: [datasourceId],
        mode: 'latest',
        callback: (data) => {
          const entry = Object.values(data[datasourceId] ?? {})[0];
          const el = this.shadowRoot?.getElementById('value');
          if (el) el.textContent = entry?.value != null ? String(entry.value) : '—';
        },
      });
    }

    disconnectedCallback() {
      this._unsubscribe?.();
    }
  }

  customElements.define('my-plugin-sensor', MySensorWidget);
})();

React example

You can build desktop widgets with React by wrapping a React component inside a custom element. Use your bundler to compile the source into a self-contained IIFE — React and ReactDOM must be bundled into the output file, not treated as externals.

The custom element acts as the bridge: it creates a DOM container in connectedCallback, mounts a React root into it, and re-renders whenever widgetApi or instanceConfig are set as DOM properties. disconnectedCallback unmounts the root to avoid memory leaks.

// widget.tsx — compiled to bundle.js with:
// esbuild widget.tsx --bundle --format=iife --jsx=automatic --outfile=bundle.js
//
// React and ReactDOM are bundled into the output automatically.
// Do not mark them as external — the host page does not expose them as globals.

import { useState, useEffect } from 'react';
import { createRoot, Root } from 'react-dom/client';

interface WidgetApi {
  subscribeTelemetry(options: {
    datasourceIds: string[];
    mode: 'latest' | 'history';
    callback: (data: Record<string, Record<string, { value?: number | string }>>) => void;
  }): () => void;
}

interface Props {
  widgetApi?: WidgetApi;
  instanceConfig?: { title?: string; datasourceId?: string };
}

function MySensorWidget({ widgetApi, instanceConfig }: Props) {
  const [value, setValue] = useState<number | string | null>(null);

  useEffect(() => {
    const datasourceId = instanceConfig?.datasourceId;
    if (!datasourceId || !widgetApi) return;
    return widgetApi.subscribeTelemetry({
      datasourceIds: [datasourceId],
      mode: 'latest',
      callback: (data) => {
        const entry = Object.values(data[datasourceId] ?? {})[0];
        setValue(entry?.value ?? null);
      },
    });
  }, [widgetApi, instanceConfig?.datasourceId]);

  return (
    <div style={{ padding: 16, fontFamily: 'sans-serif', boxSizing: 'border-box' }}>
      <div style={{ fontSize: '0.875rem', opacity: 0.6, marginBottom: 4 }}>
        {instanceConfig?.title ?? ''}
      </div>
      <div style={{ fontSize: '2rem', fontWeight: 700 }}>
        {value !== null ? String(value) : '—'}
      </div>
    </div>
  );
}

class MySensorReactWidget extends HTMLElement {
  private _root?: Root;
  private _api?: WidgetApi;
  private _config?: Props['instanceConfig'];

  connectedCallback() {
    const container = document.createElement('div');
    container.style.cssText = 'width:100%;height:100%;';
    this.appendChild(container);
    this._root = createRoot(container);
    this._render();
  }

  set widgetApi(api: WidgetApi) {
    this._api = api;
    this._render();
  }

  set instanceConfig(config: Props['instanceConfig']) {
    this._config = config;
    this._render();
  }

  private _render() {
    this._root?.render(
      <MySensorWidget widgetApi={this._api} instanceConfig={this._config} />
    );
  }

  disconnectedCallback() {
    this._root?.unmount();
  }
}

customElements.define('my-plugin-sensor', MySensorReactWidget);

WidgetApi

The widgetApi object is set on your custom element as a DOM property before or shortly after connectedCallback. Always guard against it being undefined on first render and subscribe in the set widgetApi setter.

WidgetApi reference →

© 2026 Hiclaro. All rights reserved.