Component demo · Instrument readouts and controls
Momentary Button
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>Momentary Button 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">
<div class="smart-demo-container">
<section id="overview">
<h2>Smart.MomentaryButton is an operator button with three action modes: act while held, act after being held for a set time, and act on the second press.</h2>
<p>
In momentary mode the button acts while it is pressed and stops as soon as it is released, regardless of where the pointer moved. In hold mode a ring fills while the button is held and empties if it is released early, so a short press does not start the action. In confirm mode the first press arms the button and the second press acts. An interlocked button stays readable, shows the reason and raises the <code>blocked</code> event when pressed. The button reports the operator action; the lamp shows the state reported by the plant.
</p>
</section>
<section>
<h2>A motor control station</h2>
<p>
The selector determines who controls the pump. In <em>Hand</em> the start, stop and jog buttons are active. In <em>Auto</em> they are interlocked because the sequence controls the pump, and in <em>Off</em> the pump cannot be started. Changing the selector position is a request that the simulated plant confirms after a moment. Hold <em>Start</em> for one second, and press <em>Stop</em> twice.
</p>
<div class="station">
<smart-selector-switch id="hoa" label="P-2101A control" value="hand" confirm></smart-selector-switch>
<div class="buttons">
<smart-momentary-button id="start" label="Start" mode="hold" hold-duration="1000" kind="start"></smart-momentary-button>
<smart-momentary-button id="stop" label="Stop" mode="confirm" kind="stop"></smart-momentary-button>
<smart-momentary-button id="jog" label="Jog" mode="momentary"></smart-momentary-button>
<smart-momentary-button id="estop" label="E-Stop" mode="momentary" kind="emergency"></smart-momentary-button>
</div>
<div class="plant">
<h3>Plant simulation</h3>
<div class="plant-row"><span>P-2101A</span><b id="pumpState">stopped</b></div>
<div class="plant-row"><span>Speed</span><b id="speed">0 rpm</b></div>
<ol id="log" class="log"></ol>
<div class="demo-actions">
<label><input type="checkbox" id="touch" /> Touch density</label>
<label><input type="checkbox" id="rotary" /> Rotary selector</label>
</div>
</div>
</div>
<p class="hint">
Keyboard: Tab to a button, then hold Space or Enter for the momentary and hold modes, or press twice for the confirm mode. The selector is a radio group where the arrow keys move between positions and Enter requests one.
</p>
</section>
<section>
<h2>The three action modes side by side</h2>
<p>
The same button in each mode, with the events it raises. A momentary button raises <code>press</code> and <code>release</code> with the time held. A hold button raises <code>activate</code> once the ring has filled, or <code>release</code> with <code>early</code> set when it is let go before that. A confirm button raises <code>arm</code> on the first press, <code>activate</code> on the second, and <code>disarm</code> if the second press does not come within <code>confirmTimeout</code>.
</p>
<div class="modes">
<div class="mode">
<smart-momentary-button id="modeMomentary" label="Jog" mode="momentary"></smart-momentary-button>
<span class="caption">momentary</span>
</div>
<div class="mode">
<smart-momentary-button id="modeHold" label="Purge" mode="hold" hold-duration="1500"></smart-momentary-button>
<span class="caption">hold, 1.5 s</span>
</div>
<div class="mode">
<smart-momentary-button id="modeConfirm" label="Drain" mode="confirm" confirm-timeout="3000" kind="stop"></smart-momentary-button>
<span class="caption">confirm, 3 s to press again</span>
</div>
<div class="mode">
<smart-momentary-button id="modeLocked" label="Vent" mode="momentary" interlocked interlock-reason="vessel above 2 bar"></smart-momentary-button>
<span class="caption">interlocked</span>
</div>
</div>
<ol id="modeLog" class="log"><li>Press a button.</li></ol>
</section>
</div>
<!-- scripts -->
<script type="module" src="node_modules/smart-industrial/source/modules/smart.momentarybutton.js"></script>
<script type="module" src="node_modules/smart-industrial/source/modules/smart.selectorswitch.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" />
/* A simulated pump and its control mode. The controls raise requests; the plant here
takes a moment to answer, and the lamps and the selector follow what it answers with -
never what was asked for. Replace `plant` with tag writes and a subscription. */
window.onload = function () {
/* ── The three modes side by side ─────────────────────────────────── */
const modeLog = document.getElementById('modeLog');
function modeNote(text) {
const item = document.createElement('li');
item.textContent = new Date().toLocaleTimeString('en-GB', { hour12: false }) + ' ' + text;
if (modeLog.firstChild && modeLog.firstChild.textContent === 'Press a button.') {
modeLog.removeChild(modeLog.firstChild);
}
modeLog.insertBefore(item, modeLog.firstChild);
while (modeLog.children.length > 6) {
modeLog.removeChild(modeLog.lastChild);
}
}
['modeMomentary', 'modeHold', 'modeConfirm', 'modeLocked'].forEach(function (id) {
const button = document.getElementById(id);
button.addEventListener('press', function () {
modeNote(button.label + ': press');
});
button.addEventListener('release', function (event) {
modeNote(button.label + ': release after ' + event.detail.duration + ' ms' + (event.detail.early ? ', early' : ''));
});
button.addEventListener('activate', function (event) {
modeNote(button.label + ': activate (' + event.detail.mode + ')');
});
button.addEventListener('arm', function (event) {
modeNote(button.label + ': armed, press again within ' + event.detail.timeout + ' ms');
});
button.addEventListener('disarm', function () {
modeNote(button.label + ': disarmed, no second press');
});
button.addEventListener('blocked', function (event) {
modeNote(button.label + ': blocked, ' + event.detail.reason);
});
});
const hoa = document.getElementById('hoa'),
start = document.getElementById('start'),
stop = document.getElementById('stop'),
jog = document.getElementById('jog'),
estop = document.getElementById('estop'),
pumpState = document.getElementById('pumpState'),
speedText = document.getElementById('speed'),
log = document.getElementById('log'),
touch = document.getElementById('touch'),
rotary = document.getElementById('rotary');
const plant = {
mode: 'hand',
running: false,
jogging: false,
tripped: false,
speed: 0
};
function note(text) {
const item = document.createElement('li');
item.textContent = new Date().toLocaleTimeString('en-GB', { hour12: false }) + ' ' + text;
log.insertBefore(item, log.firstChild);
while (log.children.length > 8) {
log.removeChild(log.lastChild);
}
}
/* What the plant reports, pushed onto every control. */
function publish() {
const state = plant.tripped ? 'fault' : plant.running ? 'running' : plant.jogging ? 'jogging' : 'stopped',
hand = plant.mode === 'hand';
hoa.value = plant.mode;
start.state = state;
stop.state = state;
start.interlocked = !hand || plant.tripped;
start.interlockReason = plant.tripped ? 'tripped - reset the drive' : plant.mode === 'auto' ? 'in Auto - the sequence owns the pump' : plant.mode === 'off' ? 'selector in Off' : '';
stop.interlocked = !hand;
stop.interlockReason = plant.mode === 'auto' ? 'in Auto - stop through the sequence' : plant.mode === 'off' ? 'selector in Off' : '';
jog.interlocked = !hand || plant.running || plant.tripped;
jog.interlockReason = plant.running ? 'pump running' : plant.tripped ? 'tripped' : !hand ? 'not in Hand' : '';
pumpState.textContent = state;
pumpState.className = state === 'fault' ? 'is-fault' : state === 'running' ? 'is-running' : '';
}
/* ── Selector ───────────────────────────────────────────────────────── */
hoa.addEventListener('positionRequest', function (event) {
note('positionRequest → ' + event.detail.position + ' (from ' + event.detail.from + ')');
/* The plant takes a moment, and a pump running in Hand is stopped by the move out
of Hand - which is the plant's rule, not the switch's. */
setTimeout(function () {
plant.mode = event.detail.position;
if (plant.mode !== 'hand') {
plant.running = false;
plant.speed = 0;
}
publish();
note('plant confirms mode ' + plant.mode);
}, 700);
});
hoa.addEventListener('blocked', function (event) {
note('selector blocked: ' + event.detail.reason);
});
/* ── Start: hold for a second ───────────────────────────────────────── */
start.addEventListener('activate', function () {
note('activate Start - held for ' + (start.holdDuration / 1000) + ' s');
plant.running = true;
publish();
});
start.addEventListener('release', function (event) {
if (event.detail.early) {
note('Start released after ' + event.detail.duration + ' ms - too short, nothing sent');
}
});
/* ── Stop: press twice ──────────────────────────────────────────────── */
stop.addEventListener('arm', function () {
note('Stop armed - press again within 5 s');
});
stop.addEventListener('disarm', function () {
note('Stop disarmed');
});
stop.addEventListener('activate', function () {
note('activate Stop - confirmed');
plant.running = false;
publish();
});
/* ── Jog: while held ────────────────────────────────────────────────── */
jog.addEventListener('press', function () {
note('jog press');
plant.jogging = true;
publish();
});
jog.addEventListener('release', function (event) {
note('jog release after ' + event.detail.duration + ' ms');
plant.jogging = false;
publish();
});
/* ── E-stop: acts at once, trips the drive ──────────────────────────── */
estop.addEventListener('press', function () {
note('E-STOP');
plant.tripped = true;
plant.running = false;
plant.jogging = false;
publish();
setTimeout(function () {
plant.tripped = false;
publish();
note('drive reset');
}, 6000);
});
[start, stop, jog].forEach(function (button) {
button.addEventListener('blocked', function (event) {
note(button.label + ' blocked: ' + event.detail.reason);
});
});
/* ── The speed, from the plant ───────────────────────────────────────── */
setInterval(function () {
const target = plant.tripped ? 0 : plant.running ? 1480 : plant.jogging ? 120 : 0;
plant.speed += (target - plant.speed) * 0.25;
speedText.textContent = Math.round(plant.speed) + ' rpm';
}, 200);
/* ── Chrome ─────────────────────────────────────────────────────────── */
touch.addEventListener('change', function () {
[hoa, start, stop, jog, estop].forEach(function (control) {
control.density = touch.checked ? 'touch' : 'normal';
});
});
rotary.addEventListener('change', function () {
hoa.appearance = rotary.checked ? 'rotary' : 'segmented';
});
publish();
};
styles.css
.station {
display: grid;
grid-template-columns: auto auto minmax(260px, 1fr);
gap: 24px;
align-items: start;
}
@media (max-width: 900px) {
.station {
grid-template-columns: 1fr;
}
}
.buttons {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
gap: 12px;
}
.plant h3 {
margin: 0 0 8px;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.03em;
text-transform: uppercase;
}
.plant-row {
display: flex;
justify-content: space-between;
gap: 12px;
padding: 6px 10px;
border: 1px solid var(--smart-border);
border-radius: 3px;
font-size: 13px;
margin-bottom: 4px;
}
.plant-row b {
font-variant-numeric: tabular-nums;
text-transform: uppercase;
letter-spacing: 0.04em;
font-size: 12px;
}
.is-running { color: var(--smart-success); }
.is-fault { color: var(--smart-error); }
.log {
margin: 10px 0 0;
padding: 0;
list-style: none;
font-size: 12px;
font-variant-numeric: tabular-nums;
opacity: 0.8;
}
.log li {
padding: 2px 0;
}
.demo-actions {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
margin-top: 12px;
}
.demo-actions label {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 13px;
}
.hint {
margin-top: 10px;
font-size: 12px;
opacity: 0.75;
}
.modes {
display: flex;
flex-wrap: wrap;
gap: 16px 28px;
align-items: flex-start;
}
.mode {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
}
.mode .caption {
font-size: 12px;
color: var(--smart-text-secondary-color);
}
/* ── Demo chrome, shared by the Industrial demos ─────────────────────────
Headings, prose, buttons and notes are the same on every demo page. This block
is appended by scripts and comes last, so it wins over the rules above. */
.smart-demo-container {
padding: 4px 0 32px;
}
.smart-demo-container section {
margin-bottom: 36px;
}
.smart-demo-container section h2 {
margin: 0 0 8px;
font-size: 17px;
font-weight: 600;
line-height: 1.35;
color: var(--smart-text-color);
}
.smart-demo-container #overview h2 {
margin-bottom: 10px;
font-size: 20px;
max-width: 78ch;
}
.smart-demo-container section > p {
margin: 0 0 14px;
max-width: 84ch;
font-size: 14px;
line-height: 1.55;
color: var(--smart-text-secondary-color);
}
.smart-demo-container code {
padding: 1px 5px;
border-radius: 2px;
background: var(--smart-ui-state-hover);
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
}
.smart-demo-container .demo-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 10px;
margin-top: 12px;
}
.smart-demo-container .demo-actions label {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: var(--smart-text-color);
}
.smart-demo-container .demo-actions button,
.smart-demo-container .demo-button {
min-height: 30px;
padding: 0 12px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-industrial-panel-background, var(--smart-surface));
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
font-weight: 500;
letter-spacing: 0;
text-transform: none;
cursor: pointer;
}
.smart-demo-container .demo-actions button:hover,
.smart-demo-container .demo-button:hover {
border-color: var(--smart-ui-state-border-hover);
background: var(--smart-ui-state-hover);
color: var(--smart-ui-state-color-hover);
}
.smart-demo-container .demo-actions button.demo-button-primary,
.smart-demo-container .demo-button-primary {
border-color: var(--smart-primary);
background: var(--smart-primary);
color: var(--smart-primary-color, #fff);
}
.smart-demo-container .demo-actions button[disabled],
.smart-demo-container .demo-button[disabled] {
opacity: 0.5;
cursor: default;
}
.smart-demo-container .demo-actions select,
.smart-demo-container .demo-actions input[type="text"],
.smart-demo-container .demo-actions input[type="number"] {
min-height: 30px;
padding: 2px 6px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-background);
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
}
.smart-demo-container .hint,
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
margin: 10px 0 0;
font-size: 12px;
line-height: 1.5;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
font-family: Consolas, "Courier New", monospace;
}
.smart-demo-container .log {
list-style: none;
margin: 10px 0 0;
padding: 0;
font-size: 12px;
font-variant-numeric: tabular-nums;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .log li {
padding: 2px 0;
}
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">
<div class="smart-demo-container">
<section id="overview">
<h2>Smart.MomentaryButton is an operator button with three action modes: act while held, act after being held for a set time, and act on the second press.</h2>
<p>
In momentary mode the button acts while it is pressed and stops as soon as it is released, regardless of where the pointer moved. In hold mode a ring fills while the button is held and empties if it is released early, so a short press does not start the action. In confirm mode the first press arms the button and the second press acts. An interlocked button stays readable, shows the reason and raises the <code>blocked</code> event when pressed. The button reports the operator action; the lamp shows the state reported by the plant.
</p>
</section>
<section>
<h2>A motor control station</h2>
<p>
The selector determines who controls the pump. In <em>Hand</em> the start, stop and jog buttons are active. In <em>Auto</em> they are interlocked because the sequence controls the pump, and in <em>Off</em> the pump cannot be started. Changing the selector position is a request that the simulated plant confirms after a moment. Hold <em>Start</em> for one second, and press <em>Stop</em> twice.
</p>
<div class="station">
<smart-selector-switch id="hoa" label="P-2101A control" value="hand" [confirm]="true"></smart-selector-switch>
<div class="buttons">
<smart-momentary-button id="start" label="Start" mode="hold" [holdDuration]="1000" kind="start"></smart-momentary-button>
<smart-momentary-button id="stop" label="Stop" mode="confirm" kind="stop"></smart-momentary-button>
<smart-momentary-button id="jog" label="Jog" mode="momentary"></smart-momentary-button>
<smart-momentary-button id="estop" label="E-Stop" mode="momentary" kind="emergency"></smart-momentary-button>
</div>
<div class="plant">
<h3>Plant simulation</h3>
<div class="plant-row"><span>P-2101A</span><b id="pumpState">stopped</b></div>
<div class="plant-row"><span>Speed</span><b id="speed">0 rpm</b></div>
<ol id="log" class="log"></ol>
<div class="demo-actions">
<label><input type="checkbox" id="touch" /> Touch density</label>
<label><input type="checkbox" id="rotary" /> Rotary selector</label>
</div>
</div>
</div>
<p class="hint">
Keyboard: Tab to a button, then hold Space or Enter for the momentary and hold modes, or press twice for the confirm mode. The selector is a radio group where the arrow keys move between positions and Enter requests one.
</p>
</section>
<section>
<h2>The three action modes side by side</h2>
<p>
The same button in each mode, with the events it raises. A momentary button raises <code>press</code> and <code>release</code> with the time held. A hold button raises <code>activate</code> once the ring has filled, or <code>release</code> with <code>early</code> set when it is let go before that. A confirm button raises <code>arm</code> on the first press, <code>activate</code> on the second, and <code>disarm</code> if the second press does not come within <code>confirmTimeout</code>.
</p>
<div class="modes">
<div class="mode">
<smart-momentary-button id="modeMomentary" label="Jog" mode="momentary"></smart-momentary-button>
<span class="caption">momentary</span>
</div>
<div class="mode">
<smart-momentary-button id="modeHold" label="Purge" mode="hold" [holdDuration]="1500"></smart-momentary-button>
<span class="caption">hold, 1.5 s</span>
</div>
<div class="mode">
<smart-momentary-button id="modeConfirm" label="Drain" mode="confirm" [confirmTimeout]="3000" kind="stop"></smart-momentary-button>
<span class="caption">confirm, 3 s to press again</span>
</div>
<div class="mode">
<smart-momentary-button id="modeLocked" label="Vent" mode="momentary" [interlocked]="true" interlock-reason="vessel above 2 bar"></smart-momentary-button>
<span class="caption">interlocked</span>
</div>
</div>
<ol id="modeLog" class="log"><li>Press a button.</li></ol>
</section>
</div>
</div>
angular/app.component.ts
import { Component, AfterViewInit, OnDestroy } from '@angular/core';
import { SelectorSwitchModule } from 'smart-industrial/angular/selectorswitch';
import { MomentaryButtonModule } from 'smart-industrial/angular/momentarybutton';
import { runDemo } from './demo';
/**
* Smart.UI Industrial - MomentaryButton 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: [SelectorSwitchModule, MomentaryButtonModule],
templateUrl: './app.component.html',
styleUrl: './app.component.css'
})
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. */
/* A simulated pump and its control mode. The controls raise requests; the plant here
takes a moment to answer, and the lamps and the selector follow what it answers with -
never what was asked for. Replace `plant` with tag writes and a subscription. */
export function runDemo(): void {
/* ── The three modes side by side ─────────────────────────────────── */
const modeLog = document.getElementById('modeLog');
function modeNote(text) {
const item = document.createElement('li');
item.textContent = new Date().toLocaleTimeString('en-GB', { hour12: false }) + ' ' + text;
if (modeLog.firstChild && modeLog.firstChild.textContent === 'Press a button.') {
modeLog.removeChild(modeLog.firstChild);
}
modeLog.insertBefore(item, modeLog.firstChild);
while (modeLog.children.length > 6) {
modeLog.removeChild(modeLog.lastChild);
}
}
['modeMomentary', 'modeHold', 'modeConfirm', 'modeLocked'].forEach(function (id) {
const button = document.getElementById(id);
button.addEventListener('press', function () {
modeNote(button.label + ': press');
});
button.addEventListener('release', function (event) {
modeNote(button.label + ': release after ' + event.detail.duration + ' ms' + (event.detail.early ? ', early' : ''));
});
button.addEventListener('activate', function (event) {
modeNote(button.label + ': activate (' + event.detail.mode + ')');
});
button.addEventListener('arm', function (event) {
modeNote(button.label + ': armed, press again within ' + event.detail.timeout + ' ms');
});
button.addEventListener('disarm', function () {
modeNote(button.label + ': disarmed, no second press');
});
button.addEventListener('blocked', function (event) {
modeNote(button.label + ': blocked, ' + event.detail.reason);
});
});
const hoa = document.getElementById('hoa'),
start = document.getElementById('start'),
stop = document.getElementById('stop'),
jog = document.getElementById('jog'),
estop = document.getElementById('estop'),
pumpState = document.getElementById('pumpState'),
speedText = document.getElementById('speed'),
log = document.getElementById('log'),
touch = document.getElementById('touch'),
rotary = document.getElementById('rotary');
const plant = {
mode: 'hand',
running: false,
jogging: false,
tripped: false,
speed: 0
};
function note(text) {
const item = document.createElement('li');
item.textContent = new Date().toLocaleTimeString('en-GB', { hour12: false }) + ' ' + text;
log.insertBefore(item, log.firstChild);
while (log.children.length > 8) {
log.removeChild(log.lastChild);
}
}
/* What the plant reports, pushed onto every control. */
function publish() {
const state = plant.tripped ? 'fault' : plant.running ? 'running' : plant.jogging ? 'jogging' : 'stopped',
hand = plant.mode === 'hand';
hoa.value = plant.mode;
start.state = state;
stop.state = state;
start.interlocked = !hand || plant.tripped;
start.interlockReason = plant.tripped ? 'tripped - reset the drive' : plant.mode === 'auto' ? 'in Auto - the sequence owns the pump' : plant.mode === 'off' ? 'selector in Off' : '';
stop.interlocked = !hand;
stop.interlockReason = plant.mode === 'auto' ? 'in Auto - stop through the sequence' : plant.mode === 'off' ? 'selector in Off' : '';
jog.interlocked = !hand || plant.running || plant.tripped;
jog.interlockReason = plant.running ? 'pump running' : plant.tripped ? 'tripped' : !hand ? 'not in Hand' : '';
pumpState.textContent = state;
pumpState.className = state === 'fault' ? 'is-fault' : state === 'running' ? 'is-running' : '';
}
/* ── Selector ───────────────────────────────────────────────────────── */
hoa.addEventListener('positionRequest', function (event) {
note('positionRequest → ' + event.detail.position + ' (from ' + event.detail.from + ')');
/* The plant takes a moment, and a pump running in Hand is stopped by the move out
of Hand - which is the plant's rule, not the switch's. */
setTimeout(function () {
plant.mode = event.detail.position;
if (plant.mode !== 'hand') {
plant.running = false;
plant.speed = 0;
}
publish();
note('plant confirms mode ' + plant.mode);
}, 700);
});
hoa.addEventListener('blocked', function (event) {
note('selector blocked: ' + event.detail.reason);
});
/* ── Start: hold for a second ───────────────────────────────────────── */
start.addEventListener('activate', function () {
note('activate Start - held for ' + (start.holdDuration / 1000) + ' s');
plant.running = true;
publish();
});
start.addEventListener('release', function (event) {
if (event.detail.early) {
note('Start released after ' + event.detail.duration + ' ms - too short, nothing sent');
}
});
/* ── Stop: press twice ──────────────────────────────────────────────── */
stop.addEventListener('arm', function () {
note('Stop armed - press again within 5 s');
});
stop.addEventListener('disarm', function () {
note('Stop disarmed');
});
stop.addEventListener('activate', function () {
note('activate Stop - confirmed');
plant.running = false;
publish();
});
/* ── Jog: while held ────────────────────────────────────────────────── */
jog.addEventListener('press', function () {
note('jog press');
plant.jogging = true;
publish();
});
jog.addEventListener('release', function (event) {
note('jog release after ' + event.detail.duration + ' ms');
plant.jogging = false;
publish();
});
/* ── E-stop: acts at once, trips the drive ──────────────────────────── */
estop.addEventListener('press', function () {
note('E-STOP');
plant.tripped = true;
plant.running = false;
plant.jogging = false;
publish();
setTimeout(function () {
plant.tripped = false;
publish();
note('drive reset');
}, 6000);
});
[start, stop, jog].forEach(function (button) {
button.addEventListener('blocked', function (event) {
note(button.label + ' blocked: ' + event.detail.reason);
});
});
/* ── The speed, from the plant ───────────────────────────────────────── */
setInterval(function () {
const target = plant.tripped ? 0 : plant.running ? 1480 : plant.jogging ? 120 : 0;
plant.speed += (target - plant.speed) * 0.25;
speedText.textContent = Math.round(plant.speed) + ' rpm';
}, 200);
/* ── Chrome ─────────────────────────────────────────────────────────── */
touch.addEventListener('change', function () {
[hoa, start, stop, jog, estop].forEach(function (control) {
control.density = touch.checked ? 'touch' : 'normal';
});
});
rotary.addEventListener('change', function () {
hoa.appearance = rotary.checked ? 'rotary' : 'segmented';
});
publish();
}
angular/app.component.css
.station {
display: grid;
grid-template-columns: auto auto minmax(260px, 1fr);
gap: 24px;
align-items: start;
}
@media (max-width: 900px) {
.station {
grid-template-columns: 1fr;
}
}
.buttons {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
gap: 12px;
}
.plant h3 {
margin: 0 0 8px;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.03em;
text-transform: uppercase;
}
.plant-row {
display: flex;
justify-content: space-between;
gap: 12px;
padding: 6px 10px;
border: 1px solid var(--smart-border);
border-radius: 3px;
font-size: 13px;
margin-bottom: 4px;
}
.plant-row b {
font-variant-numeric: tabular-nums;
text-transform: uppercase;
letter-spacing: 0.04em;
font-size: 12px;
}
.is-running { color: var(--smart-success); }
.is-fault { color: var(--smart-error); }
.log {
margin: 10px 0 0;
padding: 0;
list-style: none;
font-size: 12px;
font-variant-numeric: tabular-nums;
opacity: 0.8;
}
.log li {
padding: 2px 0;
}
.demo-actions {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
margin-top: 12px;
}
.demo-actions label {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 13px;
}
.hint {
margin-top: 10px;
font-size: 12px;
opacity: 0.75;
}
.modes {
display: flex;
flex-wrap: wrap;
gap: 16px 28px;
align-items: flex-start;
}
.mode {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
}
.mode .caption {
font-size: 12px;
color: var(--smart-text-secondary-color);
}
/* ── Demo chrome, shared by the Industrial demos ─────────────────────────
Headings, prose, buttons and notes are the same on every demo page. This block
is appended by scripts and comes last, so it wins over the rules above. */
.smart-demo-container {
padding: 4px 0 32px;
}
.smart-demo-container section {
margin-bottom: 36px;
}
.smart-demo-container section h2 {
margin: 0 0 8px;
font-size: 17px;
font-weight: 600;
line-height: 1.35;
color: var(--smart-text-color);
}
.smart-demo-container #overview h2 {
margin-bottom: 10px;
font-size: 20px;
max-width: 78ch;
}
.smart-demo-container section > p {
margin: 0 0 14px;
max-width: 84ch;
font-size: 14px;
line-height: 1.55;
color: var(--smart-text-secondary-color);
}
.smart-demo-container code {
padding: 1px 5px;
border-radius: 2px;
background: var(--smart-ui-state-hover);
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
}
.smart-demo-container .demo-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 10px;
margin-top: 12px;
}
.smart-demo-container .demo-actions label {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: var(--smart-text-color);
}
.smart-demo-container .demo-actions button,
.smart-demo-container .demo-button {
min-height: 30px;
padding: 0 12px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-industrial-panel-background, var(--smart-surface));
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
font-weight: 500;
letter-spacing: 0;
text-transform: none;
cursor: pointer;
}
.smart-demo-container .demo-actions button:hover,
.smart-demo-container .demo-button:hover {
border-color: var(--smart-ui-state-border-hover);
background: var(--smart-ui-state-hover);
color: var(--smart-ui-state-color-hover);
}
.smart-demo-container .demo-actions button.demo-button-primary,
.smart-demo-container .demo-button-primary {
border-color: var(--smart-primary);
background: var(--smart-primary);
color: var(--smart-primary-color, #fff);
}
.smart-demo-container .demo-actions button[disabled],
.smart-demo-container .demo-button[disabled] {
opacity: 0.5;
cursor: default;
}
.smart-demo-container .demo-actions select,
.smart-demo-container .demo-actions input[type="text"],
.smart-demo-container .demo-actions input[type="number"] {
min-height: 30px;
padding: 2px 6px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-background);
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
}
.smart-demo-container .hint,
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
margin: 10px 0 0;
font-size: 12px;
line-height: 1.5;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
font-family: Consolas, "Courier New", monospace;
}
.smart-demo-container .log {
list-style: none;
margin: 10px 0 0;
padding: 0;
font-size: 12px;
font-variant-numeric: tabular-nums;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .log li {
padding: 2px 0;
}
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 { SelectorSwitch } from 'smart-industrial/react/selectorswitch';
import { MomentaryButton } from 'smart-industrial/react/momentarybutton';
/* The demo logic is the HTML demo's: it feeds the elements the way a plant's data would. */
/* A simulated pump and its control mode. The controls raise requests; the plant here
takes a moment to answer, and the lamps and the selector follow what it answers with -
never what was asked for. Replace `plant` with tag writes and a subscription. */
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;
}
/* ── The three modes side by side ─────────────────────────────────── */
const modeLog = document.getElementById('modeLog');
function modeNote(text) {
const item = document.createElement('li');
item.textContent = new Date().toLocaleTimeString('en-GB', { hour12: false }) + ' ' + text;
if (modeLog.firstChild && modeLog.firstChild.textContent === 'Press a button.') {
modeLog.removeChild(modeLog.firstChild);
}
modeLog.insertBefore(item, modeLog.firstChild);
while (modeLog.children.length > 6) {
modeLog.removeChild(modeLog.lastChild);
}
}
['modeMomentary', 'modeHold', 'modeConfirm', 'modeLocked'].forEach(function (id) {
const button = document.getElementById(id);
button.addEventListener('press', function () {
modeNote(button.label + ': press');
});
button.addEventListener('release', function (event) {
modeNote(button.label + ': release after ' + event.detail.duration + ' ms' + (event.detail.early ? ', early' : ''));
});
button.addEventListener('activate', function (event) {
modeNote(button.label + ': activate (' + event.detail.mode + ')');
});
button.addEventListener('arm', function (event) {
modeNote(button.label + ': armed, press again within ' + event.detail.timeout + ' ms');
});
button.addEventListener('disarm', function () {
modeNote(button.label + ': disarmed, no second press');
});
button.addEventListener('blocked', function (event) {
modeNote(button.label + ': blocked, ' + event.detail.reason);
});
});
const hoa = document.getElementById('hoa'),
start = document.getElementById('start'),
stop = document.getElementById('stop'),
jog = document.getElementById('jog'),
estop = document.getElementById('estop'),
pumpState = document.getElementById('pumpState'),
speedText = document.getElementById('speed'),
log = document.getElementById('log'),
touch = document.getElementById('touch'),
rotary = document.getElementById('rotary');
const plant = {
mode: 'hand',
running: false,
jogging: false,
tripped: false,
speed: 0
};
function note(text) {
const item = document.createElement('li');
item.textContent = new Date().toLocaleTimeString('en-GB', { hour12: false }) + ' ' + text;
log.insertBefore(item, log.firstChild);
while (log.children.length > 8) {
log.removeChild(log.lastChild);
}
}
/* What the plant reports, pushed onto every control. */
function publish() {
const state = plant.tripped ? 'fault' : plant.running ? 'running' : plant.jogging ? 'jogging' : 'stopped',
hand = plant.mode === 'hand';
hoa.value = plant.mode;
start.state = state;
stop.state = state;
start.interlocked = !hand || plant.tripped;
start.interlockReason = plant.tripped ? 'tripped - reset the drive' : plant.mode === 'auto' ? 'in Auto - the sequence owns the pump' : plant.mode === 'off' ? 'selector in Off' : '';
stop.interlocked = !hand;
stop.interlockReason = plant.mode === 'auto' ? 'in Auto - stop through the sequence' : plant.mode === 'off' ? 'selector in Off' : '';
jog.interlocked = !hand || plant.running || plant.tripped;
jog.interlockReason = plant.running ? 'pump running' : plant.tripped ? 'tripped' : !hand ? 'not in Hand' : '';
pumpState.textContent = state;
pumpState.className = state === 'fault' ? 'is-fault' : state === 'running' ? 'is-running' : '';
}
/* ── Selector ───────────────────────────────────────────────────────── */
hoa.addEventListener('positionRequest', function (event) {
note('positionRequest → ' + event.detail.position + ' (from ' + event.detail.from + ')');
/* The plant takes a moment, and a pump running in Hand is stopped by the move out
of Hand - which is the plant's rule, not the switch's. */
setTimeout(function () {
plant.mode = event.detail.position;
if (plant.mode !== 'hand') {
plant.running = false;
plant.speed = 0;
}
publish();
note('plant confirms mode ' + plant.mode);
}, 700);
});
hoa.addEventListener('blocked', function (event) {
note('selector blocked: ' + event.detail.reason);
});
/* ── Start: hold for a second ───────────────────────────────────────── */
start.addEventListener('activate', function () {
note('activate Start - held for ' + (start.holdDuration / 1000) + ' s');
plant.running = true;
publish();
});
start.addEventListener('release', function (event) {
if (event.detail.early) {
note('Start released after ' + event.detail.duration + ' ms - too short, nothing sent');
}
});
/* ── Stop: press twice ──────────────────────────────────────────────── */
stop.addEventListener('arm', function () {
note('Stop armed - press again within 5 s');
});
stop.addEventListener('disarm', function () {
note('Stop disarmed');
});
stop.addEventListener('activate', function () {
note('activate Stop - confirmed');
plant.running = false;
publish();
});
/* ── Jog: while held ────────────────────────────────────────────────── */
jog.addEventListener('press', function () {
note('jog press');
plant.jogging = true;
publish();
});
jog.addEventListener('release', function (event) {
note('jog release after ' + event.detail.duration + ' ms');
plant.jogging = false;
publish();
});
/* ── E-stop: acts at once, trips the drive ──────────────────────────── */
estop.addEventListener('press', function () {
note('E-STOP');
plant.tripped = true;
plant.running = false;
plant.jogging = false;
publish();
setTimeout(function () {
plant.tripped = false;
publish();
note('drive reset');
}, 6000);
});
[start, stop, jog].forEach(function (button) {
button.addEventListener('blocked', function (event) {
note(button.label + ' blocked: ' + event.detail.reason);
});
});
/* ── The speed, from the plant ───────────────────────────────────────── */
setInterval(function () {
const target = plant.tripped ? 0 : plant.running ? 1480 : plant.jogging ? 120 : 0;
plant.speed += (target - plant.speed) * 0.25;
speedText.textContent = Math.round(plant.speed) + ' rpm';
}, 200);
/* ── Chrome ─────────────────────────────────────────────────────────── */
touch.addEventListener('change', function () {
[hoa, start, stop, jog, estop].forEach(function (control) {
control.density = touch.checked ? 'touch' : 'normal';
});
});
rotary.addEventListener('change', function () {
hoa.appearance = rotary.checked ? 'rotary' : 'segmented';
});
publish();
});
return () => {
unmounted = true;
};
}, []);
return (
<div className="react-root" ref={root} theme="industrial">
<div className="smart-demo-container">
<section id="overview">
<h2>Smart.MomentaryButton is an operator button with three action modes: act while held, act after being held for a set time, and act on the second press.</h2>
<p>In momentary mode the button acts while it is pressed and stops as soon as it is released, regardless of where the pointer moved. In hold mode a ring fills while the button is held and empties if it is released early, so a short press does not start the action. In confirm mode the first press arms the button and the second press acts. An interlocked button stays readable, shows the reason and raises the <code>blocked</code> event when pressed. The button reports the operator action; the lamp shows the state reported by the plant.</p>
</section>
<section>
<h2>A motor control station</h2>
<p>The selector determines who controls the pump. In <em>Hand</em> the start, stop and jog buttons are active. In <em>Auto</em> they are interlocked because the sequence controls the pump, and in <em>Off</em> the pump cannot be started. Changing the selector position is a request that the simulated plant confirms after a moment. Hold <em>Start</em> for one second, and press <em>Stop</em> twice.</p>
<div className="station">
<SelectorSwitch id="hoa" label="P-2101A control" value="hand" confirm></SelectorSwitch>
<div className="buttons">
<MomentaryButton id="start" label="Start" mode="hold" holdDuration={1000} kind="start"></MomentaryButton>
<MomentaryButton id="stop" label="Stop" mode="confirm" kind="stop"></MomentaryButton>
<MomentaryButton id="jog" label="Jog" mode="momentary"></MomentaryButton>
<MomentaryButton id="estop" label="E-Stop" mode="momentary" kind="emergency"></MomentaryButton>
</div>
<div className="plant">
<h3>Plant simulation</h3>
<div className="plant-row"><span>P-2101A</span><b id="pumpState">stopped</b></div>
<div className="plant-row"><span>Speed</span><b id="speed">0 rpm</b></div>
<ol id="log" className="log"></ol>
<div className="demo-actions">
<label><input type="checkbox" id="touch" /> Touch density</label>
<label><input type="checkbox" id="rotary" /> Rotary selector</label>
</div>
</div>
</div>
<p className="hint">Keyboard: Tab to a button, then hold Space or Enter for the momentary and hold modes, or press twice for the confirm mode. The selector is a radio group where the arrow keys move between positions and Enter requests one.</p>
</section>
<section>
<h2>The three action modes side by side</h2>
<p>The same button in each mode, with the events it raises. A momentary button raises <code>press</code> and <code>release</code> with the time held. A hold button raises <code>activate</code> once the ring has filled, or <code>release</code> with <code>early</code> set when it is let go before that. A confirm button raises <code>arm</code> on the first press, <code>activate</code> on the second, and <code>disarm</code> if the second press does not come within <code>confirmTimeout</code>.</p>
<div className="modes">
<div className="mode">
<MomentaryButton id="modeMomentary" label="Jog" mode="momentary"></MomentaryButton>
<span className="caption">momentary</span>
</div>
<div className="mode">
<MomentaryButton id="modeHold" label="Purge" mode="hold" holdDuration={1500}></MomentaryButton>
<span className="caption">hold, 1.5 s</span>
</div>
<div className="mode">
<MomentaryButton id="modeConfirm" label="Drain" mode="confirm" confirmTimeout={3000} kind="stop"></MomentaryButton>
<span className="caption">confirm, 3 s to press again</span>
</div>
<div className="mode">
<MomentaryButton id="modeLocked" label="Vent" mode="momentary" interlocked interlockReason="vessel above 2 bar"></MomentaryButton>
<span className="caption">interlocked</span>
</div>
</div>
<ol id="modeLog" className="log">
<li>Press a button.</li>
</ol>
</section>
</div>
</div>
);
};
export default App;
react/App.css
.station {
display: grid;
grid-template-columns: auto auto minmax(260px, 1fr);
gap: 24px;
align-items: start;
}
@media (max-width: 900px) {
.station {
grid-template-columns: 1fr;
}
}
.buttons {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
gap: 12px;
}
.plant h3 {
margin: 0 0 8px;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.03em;
text-transform: uppercase;
}
.plant-row {
display: flex;
justify-content: space-between;
gap: 12px;
padding: 6px 10px;
border: 1px solid var(--smart-border);
border-radius: 3px;
font-size: 13px;
margin-bottom: 4px;
}
.plant-row b {
font-variant-numeric: tabular-nums;
text-transform: uppercase;
letter-spacing: 0.04em;
font-size: 12px;
}
.is-running { color: var(--smart-success); }
.is-fault { color: var(--smart-error); }
.log {
margin: 10px 0 0;
padding: 0;
list-style: none;
font-size: 12px;
font-variant-numeric: tabular-nums;
opacity: 0.8;
}
.log li {
padding: 2px 0;
}
.demo-actions {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
margin-top: 12px;
}
.demo-actions label {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 13px;
}
.hint {
margin-top: 10px;
font-size: 12px;
opacity: 0.75;
}
.modes {
display: flex;
flex-wrap: wrap;
gap: 16px 28px;
align-items: flex-start;
}
.mode {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
}
.mode .caption {
font-size: 12px;
color: var(--smart-text-secondary-color);
}
/* ── Demo chrome, shared by the Industrial demos ─────────────────────────
Headings, prose, buttons and notes are the same on every demo page. This block
is appended by scripts and comes last, so it wins over the rules above. */
.smart-demo-container {
padding: 4px 0 32px;
}
.smart-demo-container section {
margin-bottom: 36px;
}
.smart-demo-container section h2 {
margin: 0 0 8px;
font-size: 17px;
font-weight: 600;
line-height: 1.35;
color: var(--smart-text-color);
}
.smart-demo-container #overview h2 {
margin-bottom: 10px;
font-size: 20px;
max-width: 78ch;
}
.smart-demo-container section > p {
margin: 0 0 14px;
max-width: 84ch;
font-size: 14px;
line-height: 1.55;
color: var(--smart-text-secondary-color);
}
.smart-demo-container code {
padding: 1px 5px;
border-radius: 2px;
background: var(--smart-ui-state-hover);
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
}
.smart-demo-container .demo-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 10px;
margin-top: 12px;
}
.smart-demo-container .demo-actions label {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: var(--smart-text-color);
}
.smart-demo-container .demo-actions button,
.smart-demo-container .demo-button {
min-height: 30px;
padding: 0 12px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-industrial-panel-background, var(--smart-surface));
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
font-weight: 500;
letter-spacing: 0;
text-transform: none;
cursor: pointer;
}
.smart-demo-container .demo-actions button:hover,
.smart-demo-container .demo-button:hover {
border-color: var(--smart-ui-state-border-hover);
background: var(--smart-ui-state-hover);
color: var(--smart-ui-state-color-hover);
}
.smart-demo-container .demo-actions button.demo-button-primary,
.smart-demo-container .demo-button-primary {
border-color: var(--smart-primary);
background: var(--smart-primary);
color: var(--smart-primary-color, #fff);
}
.smart-demo-container .demo-actions button[disabled],
.smart-demo-container .demo-button[disabled] {
opacity: 0.5;
cursor: default;
}
.smart-demo-container .demo-actions select,
.smart-demo-container .demo-actions input[type="text"],
.smart-demo-container .demo-actions input[type="number"] {
min-height: 30px;
padding: 2px 6px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-background);
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
}
.smart-demo-container .hint,
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
margin: 10px 0 0;
font-size: 12px;
line-height: 1.5;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
font-family: Consolas, "Courier New", monospace;
}
.smart-demo-container .log {
list-style: none;
margin: 10px 0 0;
padding: 0;
font-size: 12px;
font-variant-numeric: tabular-nums;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .log li {
padding: 2px 0;
}
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" v-pre>
<div class="smart-demo-container">
<section id="overview">
<h2>Smart.MomentaryButton is an operator button with three action modes: act while held, act after being held for a set time, and act on the second press.</h2>
<p>
In momentary mode the button acts while it is pressed and stops as soon as it is released, regardless of where the pointer moved. In hold mode a ring fills while the button is held and empties if it is released early, so a short press does not start the action. In confirm mode the first press arms the button and the second press acts. An interlocked button stays readable, shows the reason and raises the <code>blocked</code> event when pressed. The button reports the operator action; the lamp shows the state reported by the plant.
</p>
</section>
<section>
<h2>A motor control station</h2>
<p>
The selector determines who controls the pump. In <em>Hand</em> the start, stop and jog buttons are active. In <em>Auto</em> they are interlocked because the sequence controls the pump, and in <em>Off</em> the pump cannot be started. Changing the selector position is a request that the simulated plant confirms after a moment. Hold <em>Start</em> for one second, and press <em>Stop</em> twice.
</p>
<div class="station">
<smart-selector-switch id="hoa" label="P-2101A control" value="hand" confirm></smart-selector-switch>
<div class="buttons">
<smart-momentary-button id="start" label="Start" mode="hold" hold-duration="1000" kind="start"></smart-momentary-button>
<smart-momentary-button id="stop" label="Stop" mode="confirm" kind="stop"></smart-momentary-button>
<smart-momentary-button id="jog" label="Jog" mode="momentary"></smart-momentary-button>
<smart-momentary-button id="estop" label="E-Stop" mode="momentary" kind="emergency"></smart-momentary-button>
</div>
<div class="plant">
<h3>Plant simulation</h3>
<div class="plant-row"><span>P-2101A</span><b id="pumpState">stopped</b></div>
<div class="plant-row"><span>Speed</span><b id="speed">0 rpm</b></div>
<ol id="log" class="log"></ol>
<div class="demo-actions">
<label><input type="checkbox" id="touch" /> Touch density</label>
<label><input type="checkbox" id="rotary" /> Rotary selector</label>
</div>
</div>
</div>
<p class="hint">
Keyboard: Tab to a button, then hold Space or Enter for the momentary and hold modes, or press twice for the confirm mode. The selector is a radio group where the arrow keys move between positions and Enter requests one.
</p>
</section>
<section>
<h2>The three action modes side by side</h2>
<p>
The same button in each mode, with the events it raises. A momentary button raises <code>press</code> and <code>release</code> with the time held. A hold button raises <code>activate</code> once the ring has filled, or <code>release</code> with <code>early</code> set when it is let go before that. A confirm button raises <code>arm</code> on the first press, <code>activate</code> on the second, and <code>disarm</code> if the second press does not come within <code>confirmTimeout</code>.
</p>
<div class="modes">
<div class="mode">
<smart-momentary-button id="modeMomentary" label="Jog" mode="momentary"></smart-momentary-button>
<span class="caption">momentary</span>
</div>
<div class="mode">
<smart-momentary-button id="modeHold" label="Purge" mode="hold" hold-duration="1500"></smart-momentary-button>
<span class="caption">hold, 1.5 s</span>
</div>
<div class="mode">
<smart-momentary-button id="modeConfirm" label="Drain" mode="confirm" confirm-timeout="3000" kind="stop"></smart-momentary-button>
<span class="caption">confirm, 3 s to press again</span>
</div>
<div class="mode">
<smart-momentary-button id="modeLocked" label="Vent" mode="momentary" interlocked interlock-reason="vessel above 2 bar"></smart-momentary-button>
<span class="caption">interlocked</span>
</div>
</div>
<ol id="modeLog" class="log"><li>Press a button.</li></ol>
</section>
</div>
</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.momentarybutton.js";
import "smart-industrial/source/modules/smart.selectorswitch.js";
/* A simulated pump and its control mode. The controls raise requests; the plant here
takes a moment to answer, and the lamps and the selector follow what it answers with -
never what was asked for. Replace `plant` with tag writes and a subscription. */
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(() => {
/* ── The three modes side by side ─────────────────────────────────── */
const modeLog = document.getElementById('modeLog');
function modeNote(text) {
const item = document.createElement('li');
item.textContent = new Date().toLocaleTimeString('en-GB', { hour12: false }) + ' ' + text;
if (modeLog.firstChild && modeLog.firstChild.textContent === 'Press a button.') {
modeLog.removeChild(modeLog.firstChild);
}
modeLog.insertBefore(item, modeLog.firstChild);
while (modeLog.children.length > 6) {
modeLog.removeChild(modeLog.lastChild);
}
}
['modeMomentary', 'modeHold', 'modeConfirm', 'modeLocked'].forEach(function (id) {
const button = document.getElementById(id);
button.addEventListener('press', function () {
modeNote(button.label + ': press');
});
button.addEventListener('release', function (event) {
modeNote(button.label + ': release after ' + event.detail.duration + ' ms' + (event.detail.early ? ', early' : ''));
});
button.addEventListener('activate', function (event) {
modeNote(button.label + ': activate (' + event.detail.mode + ')');
});
button.addEventListener('arm', function (event) {
modeNote(button.label + ': armed, press again within ' + event.detail.timeout + ' ms');
});
button.addEventListener('disarm', function () {
modeNote(button.label + ': disarmed, no second press');
});
button.addEventListener('blocked', function (event) {
modeNote(button.label + ': blocked, ' + event.detail.reason);
});
});
const hoa = document.getElementById('hoa'),
start = document.getElementById('start'),
stop = document.getElementById('stop'),
jog = document.getElementById('jog'),
estop = document.getElementById('estop'),
pumpState = document.getElementById('pumpState'),
speedText = document.getElementById('speed'),
log = document.getElementById('log'),
touch = document.getElementById('touch'),
rotary = document.getElementById('rotary');
const plant = {
mode: 'hand',
running: false,
jogging: false,
tripped: false,
speed: 0
};
function note(text) {
const item = document.createElement('li');
item.textContent = new Date().toLocaleTimeString('en-GB', { hour12: false }) + ' ' + text;
log.insertBefore(item, log.firstChild);
while (log.children.length > 8) {
log.removeChild(log.lastChild);
}
}
/* What the plant reports, pushed onto every control. */
function publish() {
const state = plant.tripped ? 'fault' : plant.running ? 'running' : plant.jogging ? 'jogging' : 'stopped',
hand = plant.mode === 'hand';
hoa.value = plant.mode;
start.state = state;
stop.state = state;
start.interlocked = !hand || plant.tripped;
start.interlockReason = plant.tripped ? 'tripped - reset the drive' : plant.mode === 'auto' ? 'in Auto - the sequence owns the pump' : plant.mode === 'off' ? 'selector in Off' : '';
stop.interlocked = !hand;
stop.interlockReason = plant.mode === 'auto' ? 'in Auto - stop through the sequence' : plant.mode === 'off' ? 'selector in Off' : '';
jog.interlocked = !hand || plant.running || plant.tripped;
jog.interlockReason = plant.running ? 'pump running' : plant.tripped ? 'tripped' : !hand ? 'not in Hand' : '';
pumpState.textContent = state;
pumpState.className = state === 'fault' ? 'is-fault' : state === 'running' ? 'is-running' : '';
}
/* ── Selector ───────────────────────────────────────────────────────── */
hoa.addEventListener('positionRequest', function (event) {
note('positionRequest → ' + event.detail.position + ' (from ' + event.detail.from + ')');
/* The plant takes a moment, and a pump running in Hand is stopped by the move out
of Hand - which is the plant's rule, not the switch's. */
setTimeout(function () {
plant.mode = event.detail.position;
if (plant.mode !== 'hand') {
plant.running = false;
plant.speed = 0;
}
publish();
note('plant confirms mode ' + plant.mode);
}, 700);
});
hoa.addEventListener('blocked', function (event) {
note('selector blocked: ' + event.detail.reason);
});
/* ── Start: hold for a second ───────────────────────────────────────── */
start.addEventListener('activate', function () {
note('activate Start - held for ' + (start.holdDuration / 1000) + ' s');
plant.running = true;
publish();
});
start.addEventListener('release', function (event) {
if (event.detail.early) {
note('Start released after ' + event.detail.duration + ' ms - too short, nothing sent');
}
});
/* ── Stop: press twice ──────────────────────────────────────────────── */
stop.addEventListener('arm', function () {
note('Stop armed - press again within 5 s');
});
stop.addEventListener('disarm', function () {
note('Stop disarmed');
});
stop.addEventListener('activate', function () {
note('activate Stop - confirmed');
plant.running = false;
publish();
});
/* ── Jog: while held ────────────────────────────────────────────────── */
jog.addEventListener('press', function () {
note('jog press');
plant.jogging = true;
publish();
});
jog.addEventListener('release', function (event) {
note('jog release after ' + event.detail.duration + ' ms');
plant.jogging = false;
publish();
});
/* ── E-stop: acts at once, trips the drive ──────────────────────────── */
estop.addEventListener('press', function () {
note('E-STOP');
plant.tripped = true;
plant.running = false;
plant.jogging = false;
publish();
setTimeout(function () {
plant.tripped = false;
publish();
note('drive reset');
}, 6000);
});
[start, stop, jog].forEach(function (button) {
button.addEventListener('blocked', function (event) {
note(button.label + ' blocked: ' + event.detail.reason);
});
});
/* ── The speed, from the plant ───────────────────────────────────────── */
setInterval(function () {
const target = plant.tripped ? 0 : plant.running ? 1480 : plant.jogging ? 120 : 0;
plant.speed += (target - plant.speed) * 0.25;
speedText.textContent = Math.round(plant.speed) + ' rpm';
}, 200);
/* ── Chrome ─────────────────────────────────────────────────────────── */
touch.addEventListener('change', function () {
[hoa, start, stop, jog, estop].forEach(function (control) {
control.density = touch.checked ? 'touch' : 'normal';
});
});
rotary.addEventListener('change', function () {
hoa.appearance = rotary.checked ? 'rotary' : 'segmented';
});
publish();
});
});
}
};
</script>
<style>
.station {
display: grid;
grid-template-columns: auto auto minmax(260px, 1fr);
gap: 24px;
align-items: start;
}
@media (max-width: 900px) {
.station {
grid-template-columns: 1fr;
}
}
.buttons {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
gap: 12px;
}
.plant h3 {
margin: 0 0 8px;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.03em;
text-transform: uppercase;
}
.plant-row {
display: flex;
justify-content: space-between;
gap: 12px;
padding: 6px 10px;
border: 1px solid var(--smart-border);
border-radius: 3px;
font-size: 13px;
margin-bottom: 4px;
}
.plant-row b {
font-variant-numeric: tabular-nums;
text-transform: uppercase;
letter-spacing: 0.04em;
font-size: 12px;
}
.is-running { color: var(--smart-success); }
.is-fault { color: var(--smart-error); }
.log {
margin: 10px 0 0;
padding: 0;
list-style: none;
font-size: 12px;
font-variant-numeric: tabular-nums;
opacity: 0.8;
}
.log li {
padding: 2px 0;
}
.demo-actions {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
margin-top: 12px;
}
.demo-actions label {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 13px;
}
.hint {
margin-top: 10px;
font-size: 12px;
opacity: 0.75;
}
.modes {
display: flex;
flex-wrap: wrap;
gap: 16px 28px;
align-items: flex-start;
}
.mode {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
}
.mode .caption {
font-size: 12px;
color: var(--smart-text-secondary-color);
}
/* ── Demo chrome, shared by the Industrial demos ─────────────────────────
Headings, prose, buttons and notes are the same on every demo page. This block
is appended by scripts and comes last, so it wins over the rules above. */
.smart-demo-container {
padding: 4px 0 32px;
}
.smart-demo-container section {
margin-bottom: 36px;
}
.smart-demo-container section h2 {
margin: 0 0 8px;
font-size: 17px;
font-weight: 600;
line-height: 1.35;
color: var(--smart-text-color);
}
.smart-demo-container #overview h2 {
margin-bottom: 10px;
font-size: 20px;
max-width: 78ch;
}
.smart-demo-container section > p {
margin: 0 0 14px;
max-width: 84ch;
font-size: 14px;
line-height: 1.55;
color: var(--smart-text-secondary-color);
}
.smart-demo-container code {
padding: 1px 5px;
border-radius: 2px;
background: var(--smart-ui-state-hover);
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
}
.smart-demo-container .demo-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 10px;
margin-top: 12px;
}
.smart-demo-container .demo-actions label {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: var(--smart-text-color);
}
.smart-demo-container .demo-actions button,
.smart-demo-container .demo-button {
min-height: 30px;
padding: 0 12px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-industrial-panel-background, var(--smart-surface));
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
font-weight: 500;
letter-spacing: 0;
text-transform: none;
cursor: pointer;
}
.smart-demo-container .demo-actions button:hover,
.smart-demo-container .demo-button:hover {
border-color: var(--smart-ui-state-border-hover);
background: var(--smart-ui-state-hover);
color: var(--smart-ui-state-color-hover);
}
.smart-demo-container .demo-actions button.demo-button-primary,
.smart-demo-container .demo-button-primary {
border-color: var(--smart-primary);
background: var(--smart-primary);
color: var(--smart-primary-color, #fff);
}
.smart-demo-container .demo-actions button[disabled],
.smart-demo-container .demo-button[disabled] {
opacity: 0.5;
cursor: default;
}
.smart-demo-container .demo-actions select,
.smart-demo-container .demo-actions input[type="text"],
.smart-demo-container .demo-actions input[type="number"] {
min-height: 30px;
padding: 2px 6px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-background);
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
}
.smart-demo-container .hint,
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
margin: 10px 0 0;
font-size: 12px;
line-height: 1.5;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
font-family: Consolas, "Courier New", monospace;
}
.smart-demo-container .log {
list-style: none;
margin: 10px 0 0;
padding: 0;
font-size: 12px;
font-variant-numeric: tabular-nums;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .log li {
padding: 2px 0;
}
</style>
What the demo shows
- A motor control station
- Plant simulation
- The three action modes side by side
On the operator screens
Also in instrument readouts and controls
Bar GraphBit FieldJog PanelKnobNumeric KeypadPanel MeterSelector SwitchSeven SegmentThermometerTouch Keyboard