Component demo · Operator screens and process graphics
HMI Shell
HmiShell is the display frame for operator screens described in ISA-101.
index.htm as it is written with the smart-industrial package
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
<head>
<title>HMI Shell Overview Demo</title>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" type="text/css" href="node_modules/smart-industrial/source/styles/smart.default.css" />
<link rel="stylesheet" type="text/css" href="node_modules/smart-industrial/source/styles/smart.industrial-elements.css" />
<link rel="stylesheet" type="text/css" href="node_modules/smart-industrial/source/styles/smart.industrial.css" />
<link rel="stylesheet" type="text/css" href="node_modules/smart-industrial/source/styles/smart.industrial-dark.css" />
<link rel="stylesheet" type="text/css" href="styles.css" />
</head>
<body class="viewport" theme="industrial-dark">
<smart-hmi-shell id="shell" theme="industrial-dark" plant="Northfield plant" area="Area 2 · Utilities & Process"
screen="Plant overview" user="J. Moreau" user-role="Operator" connection="connecting"
show-theme-toggle>
<button slot="header" class="hmi-action" id="acknowledge" type="button">Acknowledge all</button>
<label slot="header" class="hmi-density"><input type="checkbox" id="touch" /> Touch density</label>
<section class="screen">
<div class="tiles">
<smart-status-tile id="tileOee" label="Overall equipment effectiveness" unit="%" precision-digits="1"></smart-status-tile>
<smart-status-tile id="tileThroughput" label="Throughput" unit="u/h" precision-digits="0"></smart-status-tile>
<smart-status-tile id="tilePressure" label="Header pressure" unit="bar" precision-digits="2"></smart-status-tile>
<smart-status-tile id="tileAlarms" label="Active alarms" precision-digits="0"></smart-status-tile>
</div>
<div class="panels">
<div class="panel">
<h2>P-2101A start permissives</h2>
<smart-permissive id="permissives" label="P-2101A" allow-bypass></smart-permissive>
</div>
<div class="panel">
<h2>PT-2101 header pressure</h2>
<smart-gauge id="gauge"></smart-gauge>
</div>
<div class="panel panel-text">
<h2>What the shell provides</h2>
<p>
The header, the navigation bar with an alarm count for each display, the main area and the alarm strip at the bottom are provided by the shell. The screen supplies its content and, through <code>slot="header"</code>, a few header controls. The shell provides the same frame on every display, which ISA-101 calls the display framework.
</p>
<p>
The connection state is shown as text. Press <em>Drop connection</em> to change it; the change is also announced to screen readers. <em>Light</em> switches the theme of the shell and of every element inside it. <em>Touch density</em> enlarges every target to 44 px for panel PCs and gloved operation.
</p>
<button class="hmi-action" id="drop" type="button">Drop connection</button>
<span id="log" class="log"></span>
</div>
</div>
</section>
</smart-hmi-shell>
<!-- scripts -->
<script type="module" src="node_modules/smart-industrial/source/modules/smart.hmishell.js"></script>
<script type="module" src="node_modules/smart-industrial/source/modules/smart.statustile.js"></script>
<script type="module" src="node_modules/smart-industrial/source/modules/smart.permissive.js"></script>
<script type="module" src="node_modules/smart-industrial/source/smart.industrial.js"></script>
<script type="module" src="index.js"></script>
<script>
/* Shown once every element has rendered, so nothing moves on the way in (styles/demos.css). */
addEventListener('load', function () {
var all = Array.prototype.slice.call(document.querySelectorAll('*')),
ready = function () { document.body.classList.add('ready'); };
Promise.all(all.map(function (e) { return e.localName.indexOf('smart-') === 0 ? customElements.whenDefined(e.localName) : null; })).then(function () {
return Promise.all(all.filter(function (e) { return typeof e.whenRendered === 'function' && !e.isRendered; }).map(function (e) { return e.whenRendered(); }));
}).then(function () { requestAnimationFrame(function () { requestAnimationFrame(ready); }); }, ready);
});
</script>
</body>
</html>
index.js
/// <reference path="../../../source/typescript/smart.elements.d.ts" />
/* The shell owns the chrome; this page owns the screen. Everything the shell shows is a
property - the connection word, the navigation list with its alarm counts, the alarm
list the footer summarises - and everything it does is an event. */
window.onload = function () {
const shell = document.getElementById('shell'),
acknowledge = document.getElementById('acknowledge'),
touch = document.getElementById('touch'),
drop = document.getElementById('drop'),
log = document.getElementById('log'),
tileOee = document.getElementById('tileOee'),
tileThroughput = document.getElementById('tileThroughput'),
tilePressure = document.getElementById('tilePressure'),
tileAlarms = document.getElementById('tileAlarms'),
permissives = document.getElementById('permissives'),
gauge = document.getElementById('gauge');
/* ── Navigation: the display hierarchy, with alarm counts per area ─── */
let alarms = [
{ id: 1, tag: 'PT-2101', message: 'Header pressure high', priority: 1, timestamp: Date.now() - 95000, acknowledged: false },
{ id: 2, tag: 'LT-2202', message: 'Tank TK-02 level low', priority: 2, timestamp: Date.now() - 240000, acknowledged: false },
{ id: 3, tag: 'HX-2108', message: 'Approach temperature high', priority: 3, timestamp: Date.now() - 900000, acknowledged: true }
];
function navigation() {
const area2 = { critical: 0, warning: 0, advisory: 0 };
alarms.forEach(function (alarm) {
if (alarm.acknowledged) {
return;
}
area2[alarm.priority === 1 ? 'critical' : alarm.priority === 2 ? 'warning' : 'advisory']++;
});
return [
{ id: 'site', label: 'Site overview', level: 1 },
{ id: 'area1', label: 'Area 1 · Intake', level: 2, alarms: { warning: 1 } },
{ id: 'area2', label: 'Area 2 · Utilities', level: 2, active: true, alarms: area2 },
{ id: 'area3', label: 'Area 3 · Packaging', level: 2 },
{ id: 'p2101', label: 'P-2101 feed pumps', level: 3 },
{ id: 'trend', label: 'Trends', level: 3 },
{ id: 'alarms', label: 'Alarm list', level: 3, alarms: area2 }
];
}
function publish() {
shell.navigation = navigation();
shell.alarms = alarms.map(function (alarm) {
return Object.assign({}, alarm);
});
const unacknowledged = alarms.filter(function (alarm) {
return !alarm.acknowledged;
}).length;
tileAlarms.value = unacknowledged;
tileAlarms.state = unacknowledged ? 'critical' : 'ok';
tileAlarms.footnote = unacknowledged ? 'see the alarm list' : 'no unacknowledged alarms';
acknowledge.disabled = unacknowledged === 0;
}
publish();
shell.addEventListener('navigate', function (event) {
log.textContent = 'navigate → ' + event.detail.id + ' (' + event.detail.item.label + ')';
/* The host changes the screen. Here it only moves the highlight. */
shell.navigation = navigation().map(function (item) {
return Object.assign({}, item, { active: item.id === event.detail.id });
});
});
shell.addEventListener('alarmsClick', function (event) {
log.textContent = 'alarmsClick → ' + event.detail.alarms.length + ' alarms; a host opens the alarm list here';
});
shell.addEventListener('userClick', function (event) {
log.textContent = 'userClick → ' + event.detail.user + '; a host opens log-off or e-signature here';
});
acknowledge.addEventListener('click', function () {
alarms = alarms.map(function (alarm) {
return Object.assign({}, alarm, { acknowledged: true });
});
publish();
log.textContent = 'acknowledged - through the host, never through the shell';
});
/* ── Theme and density ──────────────────────────────────────────────── */
shell.addEventListener('themeChange', function (event) {
/* The body carries the theme for anything outside the shell - here, nothing. */
document.body.setAttribute('theme', event.detail.theme);
log.textContent = 'theme → ' + event.detail.theme;
});
touch.addEventListener('change', function () {
shell.density = touch.checked ? 'touch' : 'normal';
});
/* ── Connection ─────────────────────────────────────────────────────── */
setTimeout(function () {
shell.connection = 'connected';
shell.connectionDetail = 'gw-02 · 40 ms';
}, 1200);
drop.addEventListener('click', function () {
if (shell.connection === 'connected') {
shell.connection = 'closed';
shell.connectionDetail = '';
drop.textContent = 'Reconnect';
['tileOee', 'tileThroughput', 'tilePressure'].forEach(function (id) {
document.getElementById(id).state = 'unknown';
});
gauge.quality = 'stale';
}
else {
shell.connection = 'reconnecting';
drop.textContent = 'Drop connection';
setTimeout(function () {
shell.connection = 'connected';
shell.connectionDetail = 'gw-02 · 40 ms';
gauge.quality = 'good';
}, 1500);
}
});
/* ── The screen itself ──────────────────────────────────────────────── */
Smart.Industrial.apply(gauge, 'pressure-gauge');
permissives.conditions = [
{ id: 'estop', label: 'Emergency stop healthy', tag: 'ES-2100', satisfied: true, timestamp: Date.now() - 86400000 },
{ id: 'suction', label: 'Suction valve open', tag: 'XV-2101', satisfied: true, timestamp: Date.now() - 7200000 },
{ id: 'level', label: 'Feed tank level above 20 %', tag: 'LT-2201', value: '14.2 %', satisfied: false, timestamp: Date.now() - 240000 },
{ id: 'remote', label: 'Local selector in REMOTE', tag: 'HS-2101', satisfied: true, timestamp: Date.now() - 18000000 }
];
let phase = 0;
setInterval(function () {
phase += 0.05;
const connected = shell.connection === 'connected';
if (!connected) {
return;
}
const oee = 86 + Math.sin(phase * 0.3) * 2,
throughput = 1150 + Math.sin(phase * 0.5) * 40,
pressure = 6.2 + Math.sin(phase) * 0.8;
tileOee.value = oee;
tileOee.state = oee < 85 ? 'warning' : 'ok';
tileThroughput.value = throughput;
tileThroughput.state = 'ok';
tilePressure.value = pressure;
tilePressure.state = pressure > 6.9 ? 'warning' : 'ok';
gauge.value = pressure;
}, 1000);
};
styles.css
html,
body {
height: 100%;
margin: 0;
}
body.viewport {
overflow: hidden;
}
smart-hmi-shell {
--smart-hmi-shell-height: 100vh;
}
.hmi-action,
.hmi-density {
display: inline-flex;
align-items: center;
gap: 6px;
min-height: var(--smart-hmi-shell-target, 28px);
padding: 0 12px;
font: inherit;
font-size: 12px;
font-weight: 500;
color: var(--smart-text-color);
background: transparent;
border: 1px solid var(--smart-industrial-panel-border, var(--smart-border));
border-radius: 3px;
cursor: pointer;
}
.hmi-action[disabled] {
opacity: 0.45;
cursor: default;
}
.screen {
display: flex;
flex-direction: column;
gap: 10px;
}
.tiles {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
gap: 10px;
}
.panels {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 10px;
}
.panel {
padding: 12px;
background: var(--smart-industrial-panel-background, var(--smart-surface));
border: 1px solid var(--smart-industrial-panel-border, var(--smart-border));
border-radius: 4px;
}
.panel h2 {
margin: 0 0 10px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.panel-text p {
margin: 0 0 10px;
font-size: 13px;
line-height: 1.5;
max-width: 60ch;
}
smart-gauge {
width: 100%;
max-width: 260px;
height: 220px;
}
.log {
display: block;
margin-top: 10px;
font-size: 12px;
opacity: 0.75;
}
The same demo in Angular, generated from the HTML demo. The frameworks guide has the project setup.
angular/app.component.html
<div class="angular-root" theme="industrial-dark">
<smart-hmi-shell id="shell" theme="industrial-dark" plant="Northfield plant" area="Area 2 · Utilities & Process"
screen="Plant overview" user="J. Moreau" user-role="Operator" connection="connecting" [showThemeToggle]="true">
<button slot="header" class="hmi-action" id="acknowledge" type="button">Acknowledge all</button>
<label slot="header" class="hmi-density"><input type="checkbox" id="touch" /> Touch density</label>
<section class="screen">
<div class="tiles">
<smart-status-tile id="tileOee" label="Overall equipment effectiveness" unit="%" [precisionDigits]="1"></smart-status-tile>
<smart-status-tile id="tileThroughput" label="Throughput" unit="u/h" [precisionDigits]="0"></smart-status-tile>
<smart-status-tile id="tilePressure" label="Header pressure" unit="bar" [precisionDigits]="2"></smart-status-tile>
<smart-status-tile id="tileAlarms" label="Active alarms" [precisionDigits]="0"></smart-status-tile>
</div>
<div class="panels">
<div class="panel">
<h2>P-2101A start permissives</h2>
<smart-permissive id="permissives" label="P-2101A" [allowBypass]="true"></smart-permissive>
</div>
<div class="panel">
<h2>PT-2101 header pressure</h2>
<smart-gauge id="gauge"></smart-gauge>
</div>
<div class="panel panel-text">
<h2>What the shell provides</h2>
<p>
The header, the navigation bar with an alarm count for each display, the main area and the alarm strip at the bottom are provided by the shell. The screen supplies its content and, through <code>slot="header"</code>, a few header controls. The shell provides the same frame on every display, which ISA-101 calls the display framework.
</p>
<p>
The connection state is shown as text. Press <em>Drop connection</em> to change it; the change is also announced to screen readers. <em>Light</em> switches the theme of the shell and of every element inside it. <em>Touch density</em> enlarges every target to 44 px for panel PCs and gloved operation.
</p>
<button class="hmi-action" id="drop" type="button">Drop connection</button>
<span id="log" class="log"></span>
</div>
</div>
</section>
</smart-hmi-shell>
</div>
angular/app.component.ts
import { Component, AfterViewInit, OnDestroy, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { HmiShellModule } from 'smart-industrial/angular/hmishell';
import { StatusTileModule } from 'smart-industrial/angular/statustile';
import { PermissiveModule } from 'smart-industrial/angular/permissive';
import 'smart-industrial/source/smart.industrial.js';
import { runDemo } from './demo';
/**
* Smart.UI Industrial - HmiShell in Angular.
*
* The template is the HTML demo's markup: the elements are the wrappers, so what is
* written on them is checked against their inputs. demo.ts drives them the way the
* plant's data would, once every element in the view has rendered.
*/
@Component({
selector: 'app-root',
standalone: true,
imports: [HmiShellModule, StatusTileModule, PermissiveModule],
templateUrl: './app.component.html',
styleUrl: './app.component.css',
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class AppComponent implements AfterViewInit, OnDestroy {
private unmounted = false;
ngAfterViewInit(): void {
const root = document.querySelector('.angular-root') as HTMLElement,
pending: Promise<void>[] = [];
root.querySelectorAll('*').forEach((element: any) => {
if (typeof element.whenRendered === 'function' && !element.isRendered) {
pending.push(element.whenRendered());
}
});
Promise.all(pending).then(() => {
if (!this.unmounted) {
runDemo();
}
});
}
ngOnDestroy(): void {
this.unmounted = true;
}
}
angular/demo.ts
// @ts-nocheck
/* The HTML demo's script, as it was written: page code that feeds the elements, not a typed
API. The types are on the wrappers in app.component.ts. */
declare const Smart: any;
/* The shell owns the chrome; this page owns the screen. Everything the shell shows is a
property - the connection word, the navigation list with its alarm counts, the alarm
list the footer summarises - and everything it does is an event. */
export function runDemo(): void {
const shell = document.getElementById('shell'),
acknowledge = document.getElementById('acknowledge'),
touch = document.getElementById('touch'),
drop = document.getElementById('drop'),
log = document.getElementById('log'),
tileOee = document.getElementById('tileOee'),
tileThroughput = document.getElementById('tileThroughput'),
tilePressure = document.getElementById('tilePressure'),
tileAlarms = document.getElementById('tileAlarms'),
permissives = document.getElementById('permissives'),
gauge = document.getElementById('gauge');
/* ── Navigation: the display hierarchy, with alarm counts per area ─── */
let alarms = [
{ id: 1, tag: 'PT-2101', message: 'Header pressure high', priority: 1, timestamp: Date.now() - 95000, acknowledged: false },
{ id: 2, tag: 'LT-2202', message: 'Tank TK-02 level low', priority: 2, timestamp: Date.now() - 240000, acknowledged: false },
{ id: 3, tag: 'HX-2108', message: 'Approach temperature high', priority: 3, timestamp: Date.now() - 900000, acknowledged: true }
];
function navigation() {
const area2 = { critical: 0, warning: 0, advisory: 0 };
alarms.forEach(function (alarm) {
if (alarm.acknowledged) {
return;
}
area2[alarm.priority === 1 ? 'critical' : alarm.priority === 2 ? 'warning' : 'advisory']++;
});
return [
{ id: 'site', label: 'Site overview', level: 1 },
{ id: 'area1', label: 'Area 1 · Intake', level: 2, alarms: { warning: 1 } },
{ id: 'area2', label: 'Area 2 · Utilities', level: 2, active: true, alarms: area2 },
{ id: 'area3', label: 'Area 3 · Packaging', level: 2 },
{ id: 'p2101', label: 'P-2101 feed pumps', level: 3 },
{ id: 'trend', label: 'Trends', level: 3 },
{ id: 'alarms', label: 'Alarm list', level: 3, alarms: area2 }
];
}
function publish() {
shell.navigation = navigation();
shell.alarms = alarms.map(function (alarm) {
return Object.assign({}, alarm);
});
const unacknowledged = alarms.filter(function (alarm) {
return !alarm.acknowledged;
}).length;
tileAlarms.value = unacknowledged;
tileAlarms.state = unacknowledged ? 'critical' : 'ok';
tileAlarms.footnote = unacknowledged ? 'see the alarm list' : 'no unacknowledged alarms';
acknowledge.disabled = unacknowledged === 0;
}
publish();
shell.addEventListener('navigate', function (event) {
log.textContent = 'navigate → ' + event.detail.id + ' (' + event.detail.item.label + ')';
/* The host changes the screen. Here it only moves the highlight. */
shell.navigation = navigation().map(function (item) {
return Object.assign({}, item, { active: item.id === event.detail.id });
});
});
shell.addEventListener('alarmsClick', function (event) {
log.textContent = 'alarmsClick → ' + event.detail.alarms.length + ' alarms; a host opens the alarm list here';
});
shell.addEventListener('userClick', function (event) {
log.textContent = 'userClick → ' + event.detail.user + '; a host opens log-off or e-signature here';
});
acknowledge.addEventListener('click', function () {
alarms = alarms.map(function (alarm) {
return Object.assign({}, alarm, { acknowledged: true });
});
publish();
log.textContent = 'acknowledged - through the host, never through the shell';
});
/* ── Theme and density ──────────────────────────────────────────────── */
shell.addEventListener('themeChange', function (event) {
/* The body carries the theme for anything outside the shell - here, nothing. */
document.body.setAttribute('theme', event.detail.theme);
log.textContent = 'theme → ' + event.detail.theme;
});
touch.addEventListener('change', function () {
shell.density = touch.checked ? 'touch' : 'normal';
});
/* ── Connection ─────────────────────────────────────────────────────── */
setTimeout(function () {
shell.connection = 'connected';
shell.connectionDetail = 'gw-02 · 40 ms';
}, 1200);
drop.addEventListener('click', function () {
if (shell.connection === 'connected') {
shell.connection = 'closed';
shell.connectionDetail = '';
drop.textContent = 'Reconnect';
['tileOee', 'tileThroughput', 'tilePressure'].forEach(function (id) {
document.getElementById(id).state = 'unknown';
});
gauge.quality = 'stale';
}
else {
shell.connection = 'reconnecting';
drop.textContent = 'Drop connection';
setTimeout(function () {
shell.connection = 'connected';
shell.connectionDetail = 'gw-02 · 40 ms';
gauge.quality = 'good';
}, 1500);
}
});
/* ── The screen itself ──────────────────────────────────────────────── */
Smart.Industrial.apply(gauge, 'pressure-gauge');
permissives.conditions = [
{ id: 'estop', label: 'Emergency stop healthy', tag: 'ES-2100', satisfied: true, timestamp: Date.now() - 86400000 },
{ id: 'suction', label: 'Suction valve open', tag: 'XV-2101', satisfied: true, timestamp: Date.now() - 7200000 },
{ id: 'level', label: 'Feed tank level above 20 %', tag: 'LT-2201', value: '14.2 %', satisfied: false, timestamp: Date.now() - 240000 },
{ id: 'remote', label: 'Local selector in REMOTE', tag: 'HS-2101', satisfied: true, timestamp: Date.now() - 18000000 }
];
let phase = 0;
setInterval(function () {
phase += 0.05;
const connected = shell.connection === 'connected';
if (!connected) {
return;
}
const oee = 86 + Math.sin(phase * 0.3) * 2,
throughput = 1150 + Math.sin(phase * 0.5) * 40,
pressure = 6.2 + Math.sin(phase) * 0.8;
tileOee.value = oee;
tileOee.state = oee < 85 ? 'warning' : 'ok';
tileThroughput.value = throughput;
tileThroughput.state = 'ok';
tilePressure.value = pressure;
tilePressure.state = pressure > 6.9 ? 'warning' : 'ok';
gauge.value = pressure;
}, 1000);
}
angular/app.component.css
html,
body {
height: 100%;
margin: 0;
}
body.viewport {
overflow: hidden;
}
smart-hmi-shell {
--smart-hmi-shell-height: 100vh;
}
.hmi-action,
.hmi-density {
display: inline-flex;
align-items: center;
gap: 6px;
min-height: var(--smart-hmi-shell-target, 28px);
padding: 0 12px;
font: inherit;
font-size: 12px;
font-weight: 500;
color: var(--smart-text-color);
background: transparent;
border: 1px solid var(--smart-industrial-panel-border, var(--smart-border));
border-radius: 3px;
cursor: pointer;
}
.hmi-action[disabled] {
opacity: 0.45;
cursor: default;
}
.screen {
display: flex;
flex-direction: column;
gap: 10px;
}
.tiles {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
gap: 10px;
}
.panels {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 10px;
}
.panel {
padding: 12px;
background: var(--smart-industrial-panel-background, var(--smart-surface));
border: 1px solid var(--smart-industrial-panel-border, var(--smart-border));
border-radius: 4px;
}
.panel h2 {
margin: 0 0 10px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.panel-text p {
margin: 0 0 10px;
font-size: 13px;
line-height: 1.5;
max-width: 60ch;
}
smart-gauge {
width: 100%;
max-width: 260px;
height: 220px;
}
.log {
display: block;
margin-top: 10px;
font-size: 12px;
opacity: 0.75;
}
The same demo in React, generated from the HTML demo. The frameworks guide has the project setup.
react/App.jsx
import 'smart-industrial/source/styles/smart.default.css';
import 'smart-industrial/source/styles/smart.industrial-elements.css';
import 'smart-industrial/source/styles/smart.industrial.css';
import 'smart-industrial/source/styles/smart.industrial-dark.css';
import './App.css';
import React from 'react';
import { HmiShell } from 'smart-industrial/react/hmishell';
import { StatusTile } from 'smart-industrial/react/statustile';
import { Permissive } from 'smart-industrial/react/permissive';
import 'smart-industrial/source/smart.industrial.js';
const Smart = window.Smart;
/* The demo logic is the HTML demo's: it feeds the elements the way a plant's data would. */
/* The shell owns the chrome; this page owns the screen. Everything the shell shows is a
property - the connection word, the navigation list with its alarm counts, the alarm
list the footer summarises - and everything it does is an event. */
const App = () => {
const root = React.useRef(null);
React.useEffect(() => {
/* The elements render after React has mounted them, and the demo's logic - written
for a page's load event - reads their parts, so it waits for every element in the
tree to have rendered first. */
const pending = Array.from(root.current.querySelectorAll('*'))
.filter((element) => typeof element.whenRendered === 'function' && !element.isRendered)
.map((element) => element.whenRendered());
let unmounted = false;
Promise.all(pending).then(() => {
if (unmounted) {
return;
}
const shell = document.getElementById('shell'),
acknowledge = document.getElementById('acknowledge'),
touch = document.getElementById('touch'),
drop = document.getElementById('drop'),
log = document.getElementById('log'),
tileOee = document.getElementById('tileOee'),
tileThroughput = document.getElementById('tileThroughput'),
tilePressure = document.getElementById('tilePressure'),
tileAlarms = document.getElementById('tileAlarms'),
permissives = document.getElementById('permissives'),
gauge = document.getElementById('gauge');
/* ── Navigation: the display hierarchy, with alarm counts per area ─── */
let alarms = [
{ id: 1, tag: 'PT-2101', message: 'Header pressure high', priority: 1, timestamp: Date.now() - 95000, acknowledged: false },
{ id: 2, tag: 'LT-2202', message: 'Tank TK-02 level low', priority: 2, timestamp: Date.now() - 240000, acknowledged: false },
{ id: 3, tag: 'HX-2108', message: 'Approach temperature high', priority: 3, timestamp: Date.now() - 900000, acknowledged: true }
];
function navigation() {
const area2 = { critical: 0, warning: 0, advisory: 0 };
alarms.forEach(function (alarm) {
if (alarm.acknowledged) {
return;
}
area2[alarm.priority === 1 ? 'critical' : alarm.priority === 2 ? 'warning' : 'advisory']++;
});
return [
{ id: 'site', label: 'Site overview', level: 1 },
{ id: 'area1', label: 'Area 1 · Intake', level: 2, alarms: { warning: 1 } },
{ id: 'area2', label: 'Area 2 · Utilities', level: 2, active: true, alarms: area2 },
{ id: 'area3', label: 'Area 3 · Packaging', level: 2 },
{ id: 'p2101', label: 'P-2101 feed pumps', level: 3 },
{ id: 'trend', label: 'Trends', level: 3 },
{ id: 'alarms', label: 'Alarm list', level: 3, alarms: area2 }
];
}
function publish() {
shell.navigation = navigation();
shell.alarms = alarms.map(function (alarm) {
return Object.assign({}, alarm);
});
const unacknowledged = alarms.filter(function (alarm) {
return !alarm.acknowledged;
}).length;
tileAlarms.value = unacknowledged;
tileAlarms.state = unacknowledged ? 'critical' : 'ok';
tileAlarms.footnote = unacknowledged ? 'see the alarm list' : 'no unacknowledged alarms';
acknowledge.disabled = unacknowledged === 0;
}
publish();
shell.addEventListener('navigate', function (event) {
log.textContent = 'navigate → ' + event.detail.id + ' (' + event.detail.item.label + ')';
/* The host changes the screen. Here it only moves the highlight. */
shell.navigation = navigation().map(function (item) {
return Object.assign({}, item, { active: item.id === event.detail.id });
});
});
shell.addEventListener('alarmsClick', function (event) {
log.textContent = 'alarmsClick → ' + event.detail.alarms.length + ' alarms; a host opens the alarm list here';
});
shell.addEventListener('userClick', function (event) {
log.textContent = 'userClick → ' + event.detail.user + '; a host opens log-off or e-signature here';
});
acknowledge.addEventListener('click', function () {
alarms = alarms.map(function (alarm) {
return Object.assign({}, alarm, { acknowledged: true });
});
publish();
log.textContent = 'acknowledged - through the host, never through the shell';
});
/* ── Theme and density ──────────────────────────────────────────────── */
shell.addEventListener('themeChange', function (event) {
/* The body carries the theme for anything outside the shell - here, nothing. */
document.body.setAttribute('theme', event.detail.theme);
log.textContent = 'theme → ' + event.detail.theme;
});
touch.addEventListener('change', function () {
shell.density = touch.checked ? 'touch' : 'normal';
});
/* ── Connection ─────────────────────────────────────────────────────── */
setTimeout(function () {
shell.connection = 'connected';
shell.connectionDetail = 'gw-02 · 40 ms';
}, 1200);
drop.addEventListener('click', function () {
if (shell.connection === 'connected') {
shell.connection = 'closed';
shell.connectionDetail = '';
drop.textContent = 'Reconnect';
['tileOee', 'tileThroughput', 'tilePressure'].forEach(function (id) {
document.getElementById(id).state = 'unknown';
});
gauge.quality = 'stale';
}
else {
shell.connection = 'reconnecting';
drop.textContent = 'Drop connection';
setTimeout(function () {
shell.connection = 'connected';
shell.connectionDetail = 'gw-02 · 40 ms';
gauge.quality = 'good';
}, 1500);
}
});
/* ── The screen itself ──────────────────────────────────────────────── */
Smart.Industrial.apply(gauge, 'pressure-gauge');
permissives.conditions = [
{ id: 'estop', label: 'Emergency stop healthy', tag: 'ES-2100', satisfied: true, timestamp: Date.now() - 86400000 },
{ id: 'suction', label: 'Suction valve open', tag: 'XV-2101', satisfied: true, timestamp: Date.now() - 7200000 },
{ id: 'level', label: 'Feed tank level above 20 %', tag: 'LT-2201', value: '14.2 %', satisfied: false, timestamp: Date.now() - 240000 },
{ id: 'remote', label: 'Local selector in REMOTE', tag: 'HS-2101', satisfied: true, timestamp: Date.now() - 18000000 }
];
let phase = 0;
setInterval(function () {
phase += 0.05;
const connected = shell.connection === 'connected';
if (!connected) {
return;
}
const oee = 86 + Math.sin(phase * 0.3) * 2,
throughput = 1150 + Math.sin(phase * 0.5) * 40,
pressure = 6.2 + Math.sin(phase) * 0.8;
tileOee.value = oee;
tileOee.state = oee < 85 ? 'warning' : 'ok';
tileThroughput.value = throughput;
tileThroughput.state = 'ok';
tilePressure.value = pressure;
tilePressure.state = pressure > 6.9 ? 'warning' : 'ok';
gauge.value = pressure;
}, 1000);
});
return () => {
unmounted = true;
};
}, []);
return (
<div className="react-root" ref={root} theme="industrial-dark">
<HmiShell id="shell" theme="industrial-dark" plant="Northfield plant" area="Area 2 · Utilities & Process" screen="Plant overview" user="J. Moreau" userRole="Operator" connection="connecting" showThemeToggle>
<button slot="header" className="hmi-action" id="acknowledge" type="button">Acknowledge all</button>
<label slot="header" className="hmi-density"><input type="checkbox" id="touch" /> Touch density</label>
<section className="screen">
<div className="tiles">
<StatusTile id="tileOee" label="Overall equipment effectiveness" unit="%" precisionDigits={1}></StatusTile>
<StatusTile id="tileThroughput" label="Throughput" unit="u/h" precisionDigits={0}></StatusTile>
<StatusTile id="tilePressure" label="Header pressure" unit="bar" precisionDigits={2}></StatusTile>
<StatusTile id="tileAlarms" label="Active alarms" precisionDigits={0}></StatusTile>
</div>
<div className="panels">
<div className="panel">
<h2>P-2101A start permissives</h2>
<Permissive id="permissives" label="P-2101A" allowBypass></Permissive>
</div>
<div className="panel">
<h2>PT-2101 header pressure</h2>
<smart-gauge id="gauge"></smart-gauge>
</div>
<div className="panel panel-text">
<h2>What the shell provides</h2>
<p>The header, the navigation bar with an alarm count for each display, the main area and the alarm strip at the bottom are provided by the shell. The screen supplies its content and, through <code>slot="header"</code>, a few header controls. The shell provides the same frame on every display, which ISA-101 calls the display framework.</p>
<p>The connection state is shown as text. Press <em>Drop connection</em> to change it; the change is also announced to screen readers. <em>Light</em> switches the theme of the shell and of every element inside it. <em>Touch density</em> enlarges every target to 44 px for panel PCs and gloved operation.</p>
<button className="hmi-action" id="drop" type="button">Drop connection</button>
<span id="log" className="log"></span>
</div>
</div>
</section>
</HmiShell>
</div>
);
};
export default App;
react/App.css
html,
body {
height: 100%;
margin: 0;
}
body.viewport {
overflow: hidden;
}
smart-hmi-shell {
--smart-hmi-shell-height: 100vh;
}
.hmi-action,
.hmi-density {
display: inline-flex;
align-items: center;
gap: 6px;
min-height: var(--smart-hmi-shell-target, 28px);
padding: 0 12px;
font: inherit;
font-size: 12px;
font-weight: 500;
color: var(--smart-text-color);
background: transparent;
border: 1px solid var(--smart-industrial-panel-border, var(--smart-border));
border-radius: 3px;
cursor: pointer;
}
.hmi-action[disabled] {
opacity: 0.45;
cursor: default;
}
.screen {
display: flex;
flex-direction: column;
gap: 10px;
}
.tiles {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
gap: 10px;
}
.panels {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 10px;
}
.panel {
padding: 12px;
background: var(--smart-industrial-panel-background, var(--smart-surface));
border: 1px solid var(--smart-industrial-panel-border, var(--smart-border));
border-radius: 4px;
}
.panel h2 {
margin: 0 0 10px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.panel-text p {
margin: 0 0 10px;
font-size: 13px;
line-height: 1.5;
max-width: 60ch;
}
smart-gauge {
width: 100%;
max-width: 260px;
height: 220px;
}
.log {
display: block;
margin-top: 10px;
font-size: 12px;
opacity: 0.75;
}
The same demo in Vue, generated from the HTML demo. The frameworks guide has the project setup.
vue/App.vue
<template>
<div class="vue-root" theme="industrial-dark" v-pre>
<smart-hmi-shell id="shell" theme="industrial-dark" plant="Northfield plant" area="Area 2 · Utilities & Process"
screen="Plant overview" user="J. Moreau" user-role="Operator" connection="connecting"
show-theme-toggle>
<button slot="header" class="hmi-action" id="acknowledge" type="button">Acknowledge all</button>
<label slot="header" class="hmi-density"><input type="checkbox" id="touch" /> Touch density</label>
<section class="screen">
<div class="tiles">
<smart-status-tile id="tileOee" label="Overall equipment effectiveness" unit="%" precision-digits="1"></smart-status-tile>
<smart-status-tile id="tileThroughput" label="Throughput" unit="u/h" precision-digits="0"></smart-status-tile>
<smart-status-tile id="tilePressure" label="Header pressure" unit="bar" precision-digits="2"></smart-status-tile>
<smart-status-tile id="tileAlarms" label="Active alarms" precision-digits="0"></smart-status-tile>
</div>
<div class="panels">
<div class="panel">
<h2>P-2101A start permissives</h2>
<smart-permissive id="permissives" label="P-2101A" allow-bypass></smart-permissive>
</div>
<div class="panel">
<h2>PT-2101 header pressure</h2>
<smart-gauge id="gauge"></smart-gauge>
</div>
<div class="panel panel-text">
<h2>What the shell provides</h2>
<p>
The header, the navigation bar with an alarm count for each display, the main area and the alarm strip at the bottom are provided by the shell. The screen supplies its content and, through <code>slot="header"</code>, a few header controls. The shell provides the same frame on every display, which ISA-101 calls the display framework.
</p>
<p>
The connection state is shown as text. Press <em>Drop connection</em> to change it; the change is also announced to screen readers. <em>Light</em> switches the theme of the shell and of every element inside it. <em>Touch density</em> enlarges every target to 44 px for panel PCs and gloved operation.
</p>
<button class="hmi-action" id="drop" type="button">Drop connection</button>
<span id="log" class="log"></span>
</div>
</div>
</section>
</smart-hmi-shell>
</div>
</template>
<script>
import { onMounted } from "vue";
import "smart-industrial/source/styles/smart.default.css";
import "smart-industrial/source/styles/smart.industrial-elements.css";
import "smart-industrial/source/styles/smart.industrial.css";
import "smart-industrial/source/styles/smart.industrial-dark.css";
import "smart-industrial/source/modules/smart.hmishell.js";
import "smart-industrial/source/modules/smart.statustile.js";
import "smart-industrial/source/modules/smart.permissive.js";
import "smart-industrial/source/smart.industrial.js";
const Smart = window.Smart;
/* The shell owns the chrome; this page owns the screen. Everything the shell shows is a
property - the connection word, the navigation list with its alarm counts, the alarm
list the footer summarises - and everything it does is an event. */
export default {
name: "app",
setup() {
onMounted(() => {
/* The elements render after Vue has mounted them, and the demo's logic - written for
a page's load event - reads their parts, so it waits for every element in the tree
to have rendered first. */
const pending = Array.from(document.querySelector(".vue-root").querySelectorAll("*"))
.filter((element) => typeof element.whenRendered === "function" && !element.isRendered)
.map((element) => element.whenRendered());
Promise.all(pending).then(() => {
const shell = document.getElementById('shell'),
acknowledge = document.getElementById('acknowledge'),
touch = document.getElementById('touch'),
drop = document.getElementById('drop'),
log = document.getElementById('log'),
tileOee = document.getElementById('tileOee'),
tileThroughput = document.getElementById('tileThroughput'),
tilePressure = document.getElementById('tilePressure'),
tileAlarms = document.getElementById('tileAlarms'),
permissives = document.getElementById('permissives'),
gauge = document.getElementById('gauge');
/* ── Navigation: the display hierarchy, with alarm counts per area ─── */
let alarms = [
{ id: 1, tag: 'PT-2101', message: 'Header pressure high', priority: 1, timestamp: Date.now() - 95000, acknowledged: false },
{ id: 2, tag: 'LT-2202', message: 'Tank TK-02 level low', priority: 2, timestamp: Date.now() - 240000, acknowledged: false },
{ id: 3, tag: 'HX-2108', message: 'Approach temperature high', priority: 3, timestamp: Date.now() - 900000, acknowledged: true }
];
function navigation() {
const area2 = { critical: 0, warning: 0, advisory: 0 };
alarms.forEach(function (alarm) {
if (alarm.acknowledged) {
return;
}
area2[alarm.priority === 1 ? 'critical' : alarm.priority === 2 ? 'warning' : 'advisory']++;
});
return [
{ id: 'site', label: 'Site overview', level: 1 },
{ id: 'area1', label: 'Area 1 · Intake', level: 2, alarms: { warning: 1 } },
{ id: 'area2', label: 'Area 2 · Utilities', level: 2, active: true, alarms: area2 },
{ id: 'area3', label: 'Area 3 · Packaging', level: 2 },
{ id: 'p2101', label: 'P-2101 feed pumps', level: 3 },
{ id: 'trend', label: 'Trends', level: 3 },
{ id: 'alarms', label: 'Alarm list', level: 3, alarms: area2 }
];
}
function publish() {
shell.navigation = navigation();
shell.alarms = alarms.map(function (alarm) {
return Object.assign({}, alarm);
});
const unacknowledged = alarms.filter(function (alarm) {
return !alarm.acknowledged;
}).length;
tileAlarms.value = unacknowledged;
tileAlarms.state = unacknowledged ? 'critical' : 'ok';
tileAlarms.footnote = unacknowledged ? 'see the alarm list' : 'no unacknowledged alarms';
acknowledge.disabled = unacknowledged === 0;
}
publish();
shell.addEventListener('navigate', function (event) {
log.textContent = 'navigate → ' + event.detail.id + ' (' + event.detail.item.label + ')';
/* The host changes the screen. Here it only moves the highlight. */
shell.navigation = navigation().map(function (item) {
return Object.assign({}, item, { active: item.id === event.detail.id });
});
});
shell.addEventListener('alarmsClick', function (event) {
log.textContent = 'alarmsClick → ' + event.detail.alarms.length + ' alarms; a host opens the alarm list here';
});
shell.addEventListener('userClick', function (event) {
log.textContent = 'userClick → ' + event.detail.user + '; a host opens log-off or e-signature here';
});
acknowledge.addEventListener('click', function () {
alarms = alarms.map(function (alarm) {
return Object.assign({}, alarm, { acknowledged: true });
});
publish();
log.textContent = 'acknowledged - through the host, never through the shell';
});
/* ── Theme and density ──────────────────────────────────────────────── */
shell.addEventListener('themeChange', function (event) {
/* The body carries the theme for anything outside the shell - here, nothing. */
document.body.setAttribute('theme', event.detail.theme);
log.textContent = 'theme → ' + event.detail.theme;
});
touch.addEventListener('change', function () {
shell.density = touch.checked ? 'touch' : 'normal';
});
/* ── Connection ─────────────────────────────────────────────────────── */
setTimeout(function () {
shell.connection = 'connected';
shell.connectionDetail = 'gw-02 · 40 ms';
}, 1200);
drop.addEventListener('click', function () {
if (shell.connection === 'connected') {
shell.connection = 'closed';
shell.connectionDetail = '';
drop.textContent = 'Reconnect';
['tileOee', 'tileThroughput', 'tilePressure'].forEach(function (id) {
document.getElementById(id).state = 'unknown';
});
gauge.quality = 'stale';
}
else {
shell.connection = 'reconnecting';
drop.textContent = 'Drop connection';
setTimeout(function () {
shell.connection = 'connected';
shell.connectionDetail = 'gw-02 · 40 ms';
gauge.quality = 'good';
}, 1500);
}
});
/* ── The screen itself ──────────────────────────────────────────────── */
Smart.Industrial.apply(gauge, 'pressure-gauge');
permissives.conditions = [
{ id: 'estop', label: 'Emergency stop healthy', tag: 'ES-2100', satisfied: true, timestamp: Date.now() - 86400000 },
{ id: 'suction', label: 'Suction valve open', tag: 'XV-2101', satisfied: true, timestamp: Date.now() - 7200000 },
{ id: 'level', label: 'Feed tank level above 20 %', tag: 'LT-2201', value: '14.2 %', satisfied: false, timestamp: Date.now() - 240000 },
{ id: 'remote', label: 'Local selector in REMOTE', tag: 'HS-2101', satisfied: true, timestamp: Date.now() - 18000000 }
];
let phase = 0;
setInterval(function () {
phase += 0.05;
const connected = shell.connection === 'connected';
if (!connected) {
return;
}
const oee = 86 + Math.sin(phase * 0.3) * 2,
throughput = 1150 + Math.sin(phase * 0.5) * 40,
pressure = 6.2 + Math.sin(phase) * 0.8;
tileOee.value = oee;
tileOee.state = oee < 85 ? 'warning' : 'ok';
tileThroughput.value = throughput;
tileThroughput.state = 'ok';
tilePressure.value = pressure;
tilePressure.state = pressure > 6.9 ? 'warning' : 'ok';
gauge.value = pressure;
}, 1000);
});
});
}
};
</script>
<style>
html,
body {
height: 100%;
margin: 0;
}
body.viewport {
overflow: hidden;
}
smart-hmi-shell {
--smart-hmi-shell-height: 100vh;
}
.hmi-action,
.hmi-density {
display: inline-flex;
align-items: center;
gap: 6px;
min-height: var(--smart-hmi-shell-target, 28px);
padding: 0 12px;
font: inherit;
font-size: 12px;
font-weight: 500;
color: var(--smart-text-color);
background: transparent;
border: 1px solid var(--smart-industrial-panel-border, var(--smart-border));
border-radius: 3px;
cursor: pointer;
}
.hmi-action[disabled] {
opacity: 0.45;
cursor: default;
}
.screen {
display: flex;
flex-direction: column;
gap: 10px;
}
.tiles {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
gap: 10px;
}
.panels {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 10px;
}
.panel {
padding: 12px;
background: var(--smart-industrial-panel-background, var(--smart-surface));
border: 1px solid var(--smart-industrial-panel-border, var(--smart-border));
border-radius: 4px;
}
.panel h2 {
margin: 0 0 10px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.panel-text p {
margin: 0 0 10px;
font-size: 13px;
line-height: 1.5;
max-width: 60ch;
}
smart-gauge {
width: 100%;
max-width: 260px;
height: 220px;
}
.log {
display: block;
margin-top: 10px;
font-size: 12px;
opacity: 0.75;
}
</style>
What the demo shows
- P-2101A start permissives
- PT-2101 header pressure
- What the shell provides