WidgetKit

App

A dashboard widget in a card: `PlacementCard` resolves a placement (a builtin kind, its stored settings, the bound data) with the same vendored engine kui-react uses and draws the card frame and the kind's view. Same locals as kui-react's `PlacementCard` props (`kind`, `title`, `settings`, `board`, `data`, `boundSlots`, `loading`, `error`, `isEditor`, `fixedHeight`, `now`, `locale`). Kinds drawn here: `stat.value`, `stat.sparkline`, `stat.attributes`, `state` (with a lamp layout), `progress`, `gauge`, `data.table`, `data.feed`, `content.markdown`, `content.image`, `content.links`, `content.embed` and the charts `chart.line`, `chart.area`, `chart.bar` (time axis, band axis and category mode), `chart.pie`, `chart.scatter`, `chart.histogram`, `heatmap` (drawn in the browser by `modules/ui/Chart` from the vendored models), and `map`, `control.button`, `control.switch`, `control.slider`, `control.setpoint`, `control.form`, `content.video` and the process kinds `scada.symbol` (one symbol from a library of about seventy) and `scada.scene` (a diagram of symbols joined by pipes, hoses, cables and wires; drawn from the model's SVG tree, the motion stops for a viewer who prefers reduced motion). A control is drawn disabled until the page says `host: { connected: true }`; a press or commit is the bubbling `kui:widget-control` event on the card (`{ placement, kind: 'run' | 'set', value, confirmation, respond }`) that the page answers (`control.form` sends ONE `set` with `{ <field key>: <value>, … }`); a click on a map marker is `kui:widget-marker-click`. The map also draws a picture or glyph per status, the `satellite` style (`host.imagery` brings the platform's own tiles), a floor plan whose address can be bound, draggable markers on a floor plan (a drop is a `set` request `{ id, x, y }` in whole pixels, sent only when the page can write) and route replay (Play / Pause, a range, 1×–10×). What the source says about its values (unit, decimals, value map, direction) is the default; a placement setting wins. The data table is server-rendered: sort, search, page and hidden columns travel in the URL (`?wk.<id>.sort=`, `viewerFromQuery`) and the page draws it again. Every case is compared with kui-react's HTML in `tests/widget-kit-parity.test.ts`; the chart kinds are compared as models (`tests/widget-kit-chart-models.test.ts`).

stat.value — number, change and updated-ago

Preview

Orders

Today
1,284 +11.9 %vs yesterday Updated 1 minute ago

CPU

CPU load
67%
Code
<%- include('modules/app/WidgetKit/PlacementCard', {
  kind: 'stat.value',
  title: 'Orders',
  settings: { label: 'Today', increaseIs: 'good', compareLabel: 'vs yesterday', showUpdated: true },
  data: { value: ordersToday, compare: ordersYesterday },
}) %>

stat.sparkline — trend and compare

Preview

Temperature

17.4°C -18.3 %over 12 h

Power

16,006 -78.4 %vs last week
Code
<%- include('modules/app/WidgetKit/PlacementCard', {
  kind: 'stat.sparkline',
  title: 'Temperature',
  settings: { showMinMax: true },
  data: { series: temperatureSeries },
}) %>

state — big word, badge, lamp

Preview

Line status

Running just now

Line status

Running just now

Pump

On just now
Code
<%- include('modules/app/WidgetKit/PlacementCard', {
  kind: 'state',
  title: 'Pump',
  settings: { layout: 'lamp', states: [{ match: 'true', label: 'On', color: 'var(--success)' }, { match: 'false', label: 'Off', color: 'var(--error)', lit: false }] },
  data: { value: pumpOn },
}) %>

data.table — source meta, row rules, search and paging in the URL

Preview

Devices

Columns
Columns
Sensor A1 Istanbul Plant Online 23.4 °C 56 %
Sensor B2 Ankara Warehouse Online 24.0 °C 34 %
Sensor C3 Izmir Port Online 20.5 °C 70 %
Sensor D4 Bursa Line 2 Warning 22.3 °C 40 %
Sensor E5 Antalya Depot Online 24.7 °C 45 %
Code
<%- include('modules/app/WidgetKit/PlacementCard', {
  kind: 'data.table',
  id: 'devices',
  title: 'Devices',
  settings: { maxRows: 5, paging: 'pages', columnPicker: true },
  data: { rows: devices },
  viewer: widgetKit.viewerFromQuery(query, 'devices'),
}) %>

content — image, links, embed

Preview
Code
<%- include('modules/app/WidgetKit/PlacementCard', {
  kind: 'content.links',
  title: 'Shortcuts',
  settings: { links: [{ label: 'Devices', href: '/devices', icon: 'fas fa-microchip' }] },
}) %>

charts — line with a right axis, area, bar (time and category)

Preview

Climate

Power draw

Daily revenue

Best sellers

Code
<%- include('modules/app/WidgetKit/PlacementCard', {
  kind: 'chart.line',
  title: 'Climate',
  settings: {
    series: [{ key: 'Temperature', unit: '°C', decimals: 1 }, { key: 'Humidity', unit: '%', axis: 'right', lineStyle: 'dashed' }],
    thresholds: [{ value: 24, label: 'Too warm', color: 'var(--error)' }],
    legendValues: ['latest', 'max'],
  },
  data: { series: climate },
}) %>

charts — pie, scatter, histogram, heatmap

Preview

Orders by channel

Load vs. temperature

Response times

Visits by weekday and hour

Code
<%- include('modules/app/WidgetKit/PlacementCard', {
  kind: 'chart.scatter',
  title: 'Load vs. temperature',
  settings: { groupColumn: 'group', xUnit: '%', yUnit: '°C', trendLine: true },
  data: { points: loadVsTemperature },
}) %>

map — status colours, legend, zones, routes, layer switch and clickable markers

Preview

Sites

Loading map…
Layers
  • Online5
  • Warning1
  • Offline1

Last marker click: none yet

Code
<%- include('modules/app/WidgetKit/PlacementCard', {
  kind: 'map',
  id: 'sites',
  title: 'Sites',
  settings: { labelColumn: 'label', idColumn: 'id', colorColumn: 'status', tooltipColumns: ['devices', 'uptime'], linkTemplate: '/sites/{id}' },
  data: { points: sites, zones, routes },
  host: { hasMarkerClick: true },
}) %>
<script>
  // A click on a marker: { placement, id, row, href }. Cancel it to stay on the page; otherwise the browser goes to href.
  document.addEventListener('kui:widget-marker-click', function (e) { openSite(e.detail.row); e.preventDefault(); });
</script>

map — a floor plan instead of tiles, markers in picture pixels

Preview

Floor 1

Loading map…
Code
<%- include('modules/app/WidgetKit/PlacementCard', {
  kind: 'map',
  title: 'Floor 1',
  settings: { mapKind: 'image', imageSrc: '/plans/floor1.png', imageWidth: 1440, imageHeight: 900, latColumn: 'y', lngColumn: 'x', labelColumn: 'name' },
  data: { points: sensors },
}) %>

map — a picture or glyph per status, and the satellite style

Preview

Status pictures

Loading map…
  • Online5
  • Degraded0
  • Offline1
  • Warning1

Satellite

Loading map…
  • Online5
  • Warning1
  • Offline1
Code
<%- include('modules/app/WidgetKit/PlacementCard', {
  kind: 'map',
  title: 'Status pictures',
  settings: {
    colorColumn: 'status', markerStyle: 'dot', markerSize: 28,
    // A status brings its own glyph (Font Awesome class) or picture (an internal path or https; the picture wins).
    valueColors: [
      { value: 'online', color: 'var(--success)', icon: 'fas fa-circle-check' },
      { value: 'offline', color: 'var(--error)', image: '/files/pump-alarm.svg' },
    ],
  },
  data: { points: sites },
}) %>
<%- include('modules/app/WidgetKit/PlacementCard', {
  kind: 'map',
  title: 'Satellite',
  settings: { tileStyle: 'satellite' },
  data: { points: sites },
  host: { imagery: { url: 'https://tiles.example.com/{z}/{x}/{y}.jpg', attribution: '&copy; Example' } },   // optional: Esri World Imagery otherwise
}) %>

map — a floor plan with draggable markers (a drop is a set request { id, x, y })

Preview

Floor 1: drag a sensor

Loading map…

Last drop: none yet

Code
<%- include('modules/app/WidgetKit/PlacementCard', {
  kind: 'map',
  id: 'plan-edit',
  title: 'Floor 1',
  settings: { mapKind: 'image', imageWidth: 1440, imageHeight: 900, draggable: true, idColumn: 'id', latColumn: 'y', lngColumn: 'x' },
  data: { points: sensors, image: floorPlanAddress },   // image: an optional bound address that wins over settings.imageSrc
  host: { connected: true },   // markers are draggable only when the page can write (and has no disabledReason)
}) %>
<script>
  // A drop: { placement, kind: 'set', value: { id, x, y } (whole pixels from the top-left, inside the picture), respond }
  document.addEventListener('kui:widget-control', function (e) {
    var d = e.detail;
    d.respond(fetch('/api/plans/' + d.placement, { method: 'POST', body: JSON.stringify(d.value) }).then(function (r) { if (!r.ok) throw new Error('Refused'); }));
  });
</script>

map — route replay

Preview

Deliveries

Loading map…
0 %
Code
<%- include('modules/app/WidgetKit/PlacementCard', {
  kind: 'map',
  title: 'Deliveries',
  settings: { replay: true, replaySpeed: '2' },
  // A route row's positions: [[lat, lng], …]. A third value per point (a time) makes the replay follow the clock.
  data: { points: [], routes: deliveries },
}) %>

control.form — several fields saved as one request

Preview

Drilling parameters

Set and save together
m
m/h
L/s

Pump settings

General
Limits
m

Last save: none yet

Code
<%- include('modules/app/WidgetKit/PlacementCard', {
  kind: 'control.form',
  id: 'pump-form',
  title: 'Pump settings',
  settings: { fields: [
    { key: 'name', label: 'Name', type: 'text', required: true },
    { key: 'lowOn', label: 'Low limit on', type: 'boolean' },
    { key: 'low', label: 'Low limit', type: 'number', min: 0, max: 10, unit: 'm', gate: 'lowOn' },   // switched off, and sent as null, while lowOn is false
  ] },
  data: { record: pumpRecord },   // with no fields the source's own record fields are used (unit, decimals, value map)
  host: { connected: true, confirm: { mode: 'none' } },
}) %>
<script>
  // Save sends ONE request: { placement, kind: 'set', value: { name, lowOn, low }, confirmation, respond(promise) }.
  // The page checks and writes it (the command picks the fields it wants by key), then tells the form the new record:
  //   document.querySelector('[data-kui-control="form"]').__kuiControl.setValue(record)
  document.addEventListener('kui:widget-control', function (e) { var d = e.detail; d.respond(save(d.value)); });
</script>

scada.symbol — process symbols from the readings (idle, running, warning, critical)

Preview

Storage tank

Circulation pump

Dial

Solenoid valve

Code
<%- include('modules/app/WidgetKit/PlacementCard', {
  kind: 'scada.symbol',
  title: 'Storage tank',
  settings: { symbol: 'tank.vertical', max: 100, label: 'Tank A' },
  data: { value: level, running: pumpOn },   // value, value2, running, status | warning | critical, label
}) %>

scada.scene — a pool plant and a drilling rig drawn from one table of readings

Preview

Pool plant

198.0Pool · 55 000 L72.0StrainerCirculation pumpSand filterSolenoid valveHeat pumpDrain valveDrainPressure

Drilling rig

020406080100020406080100Well depth (m)1,930.0DrawworksMud pumpValveTank 128.0DegasserTank 227.0
Code
<%- include('modules/app/WidgetKit/PlacementCard', {
  kind: 'scada.scene',
  title: 'Pool plant',
  settings: {
    width: 1240, height: 640, background: 'dots',
    elements: [{ id: 'pump', symbol: 'pump.volute', x: 345, y: 360, label: 'Circulation pump' }, { id: 'flt', symbol: 'filter.sand', x: 490, y: 360 }],
    links: [{ from: 'pump.r', to: 'flt.l', kind: 'pipe', medium: 'cold', arrows: true }],
  },
  data: { points: readings },   // one row per symbol id: { id, value, value2, running, status, label }
}) %>

scada.scene sector packs — titles, zones and dividers (shapes), the sld link kind, the calm look

Preview

Building HVAC

68.020.821.524.2Air-cooled chillerChilled water pumpsBuffer tank24.0Heating boiler 68.0Heating pumpsValveRadiatorFan coilOffice building · HVACPlant roomAir handlingLobby · hot water terminalsOpen office · VAV 1Meeting · VAV 2Outdoor airAir handling unitHeat recoveryHeating loopChilled water loop

Plant substation

51M184.622.038.4Utility 11 kVIsolatorCTOvercurrentIncomer Q1MV busbarDrive: checkPump M1MeterEV charging bayBuilding servicesSolar arrayPlant substation · single-line diagramT1 · 11/0.4 kV · 1 MVAT2 · 11/0.4 kV · 1 MVAVTQ2Q3Q4EnergisedWarningDe-energisedUtility incomer, 11 kV switchboard and transformersLV board A · 400 V · processLV board B · 400 V · site services
Code
<%- include('modules/app/WidgetKit/PlacementCard', {
  kind: 'scada.scene',
  title: 'Substation',
  settings: {
    width: 900, height: 420, background: 'dots',
    calm: true,   // ISA-101: running parts are not green, only a warning or an alarm is coloured
    shapes: [
      { kind: 'text', x: 20, y: 30, text: 'Plant substation', size: 'l', align: 'start' },
      { kind: 'zone', x: 10, y: 56, w: 420, h: 340, text: 'Medium voltage', tone: 'info' },
      { kind: 'divider', x: 440, y: 56, w: 0, h: 340 },
    ],
    elements: [{ id: 'grid', symbol: 'sld.grid', x: 70, y: 240 }, { id: 'cb1', symbol: 'sld.breaker', x: 170, y: 240 }],
    links: [{ from: 'grid.r', to: 'cb1.l', kind: 'sld' }],
  },
  data: { points: readings },
}) %>

scada.scene Smart home and Symbol states — the home, appliance and gadget packs, the states layer (mode, quality, speed, direction, ack, position, pending, failed)

Preview

Smart home

26.4→ 22.024.5→ 22.09.038m58.058.07.4 kW-18.041.0Air conditionerCeiling lightThermostatBlinds70.0Floor lampTelevisionTower fanSmoke detectorFridgeOven180.0CooktopDishwasherMicrowaveCoffeeKettle23.0Robot vacuum74.0Washer62.0DryerDehumidifierWater heaterRadiatorCeiling fanHumidifier46.0Panel heater21.0Blinds20.0Bedside LED stripAir purifier82.0Smart plug85.0Garage door0.0EV charger64.0Door lockChest freezerGarage lightRobot mower81.0IrrigationGarden lampPool pump70.0Smart home · ground floor and gardenLiving roomKitchenLaundry and utilityBedroomGarageGarden

Symbol states

M?L!M?L!M?L!M?L!IIMIOII?OIIIL!IISymbol statesRunningManualMaintenanceLocked outStale dataBad dataLostAlarm, newCommand sentCommand failedHalf speedReversed
Code
<%- include('modules/app/WidgetKit/PlacementCard', {
  kind: 'scada.scene',
  title: 'Symbol states',
  settings: {
    width: 400, height: 220, background: 'dots',
    elements: [{ id: 'pump', symbol: 'pump.motor', x: 110, y: 110 }, { id: 'valve', symbol: 'valve.ball', x: 290, y: 110 }],
  },
  // The states are columns of the row: mode (auto manual local maintenance lockout simulation), quality (good stale bad offline),
  // speed (percent; negative or direction 'reverse' turns the other way), ack, position (0-100), pending, failed.
  // A symbol in lockout keeps its hit area but runs no gesture (aria-disabled, data-locked).
  data: { points: [
    { id: 'pump', running: true, mode: 'maintenance', quality: 'stale', speed: 50 },
    { id: 'valve', running: true, position: 40, pending: true },
  ] },
}) %>

controls — button, switch, slider and setpoint; the page answers kui:widget-control

Preview

Restart line

Restarts conveyor 2 after a stop

Coolant pump

Pump power
On

Fan speed

Speed
67 %
0 % 100 %

Thermostat

Target temperature 10.0 °C – 30.0 °C
Now 22.0°C
°C

Boiler

Now 65°C
65°C

Not connected

No host
On
Not connected: no command is set for this control.

Last request: none yet

Code
<%- include('modules/app/WidgetKit/PlacementCard', {
  kind: 'control.switch',
  id: 'pump',
  title: 'Coolant pump',
  settings: { label: 'Pump power', stateDisplay: 'pill' },
  data: { value: pumpOn },
  host: { connected: true, confirm: { mode: 'none' } },
}) %>
<script>
  // { placement, kind: 'run' | 'set', value (set only), confirmation, respond(promise) }
  document.addEventListener('kui:widget-control', function (e) {
    var d = e.detail;
    d.respond(fetch('/api/controls/' + d.placement, { method: 'POST', body: JSON.stringify(d) }).then(function (r) { if (!r.ok) throw new Error('Refused'); }));
  });
</script>

content.video — a file with subtitles and a stream through hls.js

Preview

Intro

Intro

0:00/0:00

Gate camera

0:00/0:00
Code
<%- include('modules/app/WidgetKit/PlacementCard', {
  kind: 'content.video',
  title: 'Gate camera',
  settings: { src: 'https://cdn.example.com/live/gate.m3u8', autoPlay: true },
}) %>

Card states — loading, error, no data, not configured

Preview

Loading

Error

No data

No data

Not configured

Bind data to this widget in its settings.

Code
<%- include('modules/app/WidgetKit/PlacementCard', { kind: 'stat.value', title: 'Loading', loading: true, boundSlots: ['value'] }) %>
Source modules/app/WidgetKit/PlacementCard.ejs