WidgetKit
AppA 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`).
Orders
CPU
<%- 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 },
}) %>
Temperature
Power
<%- include('modules/app/WidgetKit/PlacementCard', {
kind: 'stat.sparkline',
title: 'Temperature',
settings: { showMinMax: true },
data: { series: temperatureSeries },
}) %>
Line status
Line status
Pump
<%- 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 },
}) %>
Devices
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 % |
<%- 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'),
}) %>
Plan
Shortcuts
<%- include('modules/app/WidgetKit/PlacementCard', {
kind: 'content.links',
title: 'Shortcuts',
settings: { links: [{ label: 'Devices', href: '/devices', icon: 'fas fa-microchip' }] },
}) %>
Climate
Power draw
Daily revenue
Best sellers
<%- 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 },
}) %>
Orders by channel
Load vs. temperature
Response times
Visits by weekday and hour
<%- include('modules/app/WidgetKit/PlacementCard', {
kind: 'chart.scatter',
title: 'Load vs. temperature',
settings: { groupColumn: 'group', xUnit: '%', yUnit: '°C', trendLine: true },
data: { points: loadVsTemperature },
}) %>
Sites
- Online5
- Warning1
- Offline1
Last marker click:
<%- 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>
Floor 1
<%- 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 },
}) %>
Status pictures
- Online5
- Degraded0
- Offline1
- Warning1
Satellite
- Online5
- Warning1
- Offline1
<%- 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: '© Example' } }, // optional: Esri World Imagery otherwise
}) %>
Floor 1: drag a sensor
Last drop:
<%- 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>
Deliveries
<%- 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 },
}) %>
Drilling parameters
Pump settings
Write these settings?
Last save:
<%- 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>
Storage tank
Circulation pump
Dial
Solenoid valve
<%- 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
}) %>
Pool plant
Drilling rig
<%- 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 }
}) %>
Building HVAC
Plant substation
<%- 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 },
}) %>
Smart home
Symbol states
<%- 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 },
] },
}) %>
Restart line
Coolant pump
Fan speed
Change the fan speed?
Thermostat
Boiler
Current: 65
Not connected
Last request:
<%- 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>
Intro
Intro
Gate camera
<%- include('modules/app/WidgetKit/PlacementCard', {
kind: 'content.video',
title: 'Gate camera',
settings: { src: 'https://cdn.example.com/live/gate.m3u8', autoPlay: true },
}) %>
Loading
Error
The source did not answer.
No data
No data
Not configured
Bind data to this widget in its settings.
<%- include('modules/app/WidgetKit/PlacementCard', { kind: 'stat.value', title: 'Loading', loading: true, boundSlots: ['value'] }) %>
<%#
One placement: its resolved settings, the frame and the type's view (kui-react PlacementCard). All logic is the
vendored WidgetKit's (src/widget-kit); this partial only draws it.
Locals (the names of kui-react's PlacementCard props): kind, title, settings, board, definitionDefaults, data,
boundSlots, loading, error, isEditor, fixedHeight, now, locale, messages, embedHosts, refresh, retry.
`widgetKit` is app.locals.widgetKit (src/widget-kit-host).
%><%_
// Without a kind (or the host helper) there is nothing to draw: the empty-state contract of every module partial.
if (!locals.widgetKit || !locals.kind) return;
var widgetKit = locals.widgetKit;
var _p = widgetKit.placement(locals);
_%>
<%- include('WidgetCard', { p: _p, locale: _p.locale, now: _p.now, messages: _p.messages }) %>
<%- include('_runtime') %>