Component demo · Maintenance and sites
Asset Card
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>Asset Card 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.AssetCard is one machine as maintenance sees it: how long it has run, when it is due, and what is open against it.</h2>
<p>The run hours and starts come from the PLC's counters, the service interval is the maker's, and the
condition readings are whatever the plant measures. The card's first job is to answer "is it due" in hours,
before anybody opens a maintenance system. Colour is kept for what is abnormal - a service due soon or overdue,
a reading out of band, a machine in fault - and never for a machine that is simply running.</p>
</section>
<section>
<h2>Two pumps</h2>
<p>The feed pump is running and due for service soon, with its drive-end vibration above the warning band. The
dosing pump has tripped and is past its service. <b>Request work</b> and <b>Record service</b> raise events;
the page plays the station that keeps them.</p>
<div class="demo-row">
<smart-asset-card id="pump" name="Feed pump P-101" asset-id="P-101" model="KSB Etanorm 65-200" location="Pump house, bay 2" service-interval="2000"></smart-asset-card>
<smart-asset-card id="dosing" name="Dosing pump P-204" asset-id="P-204" model="Grundfos DDA 7.5" location="Chemical room" service-interval="4000" state="fault"></smart-asset-card>
</div>
<p class="hint" id="note">The card raises <code>workOrderRequest</code>, <code>serviceRecord</code> and <code>workOrderOpen</code>.</p>
</section>
<section>
<h2>Compact, for a wall of assets</h2>
<p><code>compact</code> keeps the name, the state and the service: enough to see, across a line, which machine
is next.</p>
<div class="demo-row wall">
<smart-asset-card compact read-only name="Infeed conveyor" asset-id="CV-301" service-interval="3000" run-hours="8120" last-service-hours="6000"></smart-asset-card>
<smart-asset-card compact read-only name="Filler" asset-id="FL-310" service-interval="1000" run-hours="8120" last-service-hours="7200"></smart-asset-card>
<smart-asset-card compact read-only name="Capper" asset-id="CP-320" service-interval="1500" run-hours="8120" last-service-hours="6500" state="maintenance"></smart-asset-card>
<smart-asset-card compact read-only name="Labeller" asset-id="LB-330" service-interval="2500" run-hours="8120" last-service-hours="7000" state="stopped"></smart-asset-card>
</div>
</section>
</div>
<!-- scripts -->
<script type="module" src="node_modules/smart-industrial/source/modules/smart.assetcard.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 asset card: the page plays the station, which reads the run-hour counter, keeps the work
orders and records a service against the hours it was done at. */
window.onload = function () {
const pump = document.getElementById('pump'),
dosing = document.getElementById('dosing'),
note = document.getElementById('note'),
day = (days) => new Date(Date.now() + days * 86400000).toISOString(),
orders = {
'P-101': [
{ id: 'WO-1182', title: 'Drive-end bearing noise', status: 'open', priority: 'high', due: day(2) },
{ id: 'WO-1175', title: 'Replace mechanical seal kit', status: 'in-progress', priority: 'normal', due: day(6) }
],
'P-204': [
{ id: 'WO-1190', title: 'Dosing pump tripped on overpressure', status: 'open', priority: 'high', due: day(0) }
]
};
let next = 1191;
Object.assign(pump, {
runHours: 12640,
starts: 3214,
lastServiceHours: 10800,
lastServiceDate: day(-190),
lastServiceBy: 'J. Novak',
condition: [
{ label: 'Vibration, drive end', value: 4.6, unit: 'mm/s', status: 'warning', limit: 4.5 },
{ label: 'Vibration, non-drive end', value: 2.1, unit: 'mm/s', status: 'normal' },
{ label: 'Bearing temperature', value: 61, unit: '°C', status: 'normal' },
{ label: 'Motor current', value: 38.4, unit: 'A', status: 'normal' }
],
workOrders: orders['P-101'].slice()
});
Object.assign(dosing, {
runHours: 21460,
starts: 18230,
lastServiceHours: 17200,
lastServiceDate: day(-400),
lastServiceBy: 'J. Novak',
condition: [
{ label: 'Discharge pressure', value: 11.8, unit: 'bar', status: 'alarm', limit: 10 },
{ label: 'Stroke rate', value: 0, unit: '/min', status: 'normal' }
],
workOrders: orders['P-204'].slice()
});
/* The run-hour counter ticks on the feed pump while it runs (sped up). */
setInterval(function () {
if (pump.state === 'running') {
pump.runHours += 1;
}
}, 4000);
[pump, dosing].forEach(function (card) {
card.addEventListener('workOrderRequest', function (event) {
const detail = event.detail;
card.busy = true;
note.textContent = 'Sending the request...';
setTimeout(function () {
card.busy = false;
orders[detail.assetId].unshift({ id: 'WO-' + next++, title: detail.text, status: 'open', priority: detail.priority });
card.workOrders = orders[detail.assetId].slice();
note.textContent = 'Work order WO-' + (next - 1) + ' opened on ' + detail.assetId + ' (' + detail.priority + '): "' + detail.text + '"';
}, 500);
});
card.addEventListener('serviceRecord', function (event) {
const detail = event.detail;
card.lastServiceHours = detail.runHours;
card.lastServiceDate = new Date().toISOString();
card.lastServiceBy = 'A. Brandt';
note.textContent = 'Service of ' + detail.assetId + ' recorded at ' + detail.runHours + ' h: the interval starts again.';
});
card.addEventListener('workOrderOpen', function (event) {
note.textContent = 'Open ' + event.detail.id + ' in the maintenance system.';
});
});
};
styles.css
/* Cards at the width they would have on a maintenance screen. */
.demo-row {
display: flex;
flex-wrap: wrap;
gap: 24px;
align-items: flex-start;
}
smart-asset-card {
width: 380px;
max-width: 100%;
}
.demo-row.wall {
gap: 12px;
}
.demo-row.wall smart-asset-card {
width: 240px;
}
/* ── 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.AssetCard is one machine as maintenance sees it: how long it has run, when it is due, and what is open against it.</h2>
<p>The run hours and starts come from the PLC's counters, the service interval is the maker's, and the
condition readings are whatever the plant measures. The card's first job is to answer "is it due" in hours,
before anybody opens a maintenance system. Colour is kept for what is abnormal - a service due soon or overdue,
a reading out of band, a machine in fault - and never for a machine that is simply running.</p>
</section>
<section>
<h2>Two pumps</h2>
<p>The feed pump is running and due for service soon, with its drive-end vibration above the warning band. The
dosing pump has tripped and is past its service. <b>Request work</b> and <b>Record service</b> raise events;
the page plays the station that keeps them.</p>
<div class="demo-row">
<smart-asset-card id="pump" name="Feed pump P-101" asset-id="P-101" model="KSB Etanorm 65-200" location="Pump house, bay 2" [serviceInterval]="2000"></smart-asset-card>
<smart-asset-card id="dosing" name="Dosing pump P-204" asset-id="P-204" model="Grundfos DDA 7.5" location="Chemical room" [serviceInterval]="4000" state="fault"></smart-asset-card>
</div>
<p class="hint" id="note">The card raises <code>workOrderRequest</code>, <code>serviceRecord</code> and <code>workOrderOpen</code>.</p>
</section>
<section>
<h2>Compact, for a wall of assets</h2>
<p><code>compact</code> keeps the name, the state and the service: enough to see, across a line, which machine
is next.</p>
<div class="demo-row wall">
<smart-asset-card [compact]="true" [readOnly]="true" name="Infeed conveyor" asset-id="CV-301" [serviceInterval]="3000" [runHours]="8120" [lastServiceHours]="6000"></smart-asset-card>
<smart-asset-card [compact]="true" [readOnly]="true" name="Filler" asset-id="FL-310" [serviceInterval]="1000" [runHours]="8120" [lastServiceHours]="7200"></smart-asset-card>
<smart-asset-card [compact]="true" [readOnly]="true" name="Capper" asset-id="CP-320" [serviceInterval]="1500" [runHours]="8120" [lastServiceHours]="6500" state="maintenance"></smart-asset-card>
<smart-asset-card [compact]="true" [readOnly]="true" name="Labeller" asset-id="LB-330" [serviceInterval]="2500" [runHours]="8120" [lastServiceHours]="7000" state="stopped"></smart-asset-card>
</div>
</section>
</div>
</div>
angular/app.component.ts
import { Component, AfterViewInit, OnDestroy } from '@angular/core';
import { AssetCardModule } from 'smart-industrial/angular/assetcard';
import { runDemo } from './demo';
/**
* Smart.UI Industrial - AssetCard 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: [AssetCardModule],
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. */
/* The asset card: the page plays the station, which reads the run-hour counter, keeps the work
orders and records a service against the hours it was done at. */
export function runDemo(): void {
const pump = document.getElementById('pump'),
dosing = document.getElementById('dosing'),
note = document.getElementById('note'),
day = (days) => new Date(Date.now() + days * 86400000).toISOString(),
orders = {
'P-101': [
{ id: 'WO-1182', title: 'Drive-end bearing noise', status: 'open', priority: 'high', due: day(2) },
{ id: 'WO-1175', title: 'Replace mechanical seal kit', status: 'in-progress', priority: 'normal', due: day(6) }
],
'P-204': [
{ id: 'WO-1190', title: 'Dosing pump tripped on overpressure', status: 'open', priority: 'high', due: day(0) }
]
};
let next = 1191;
Object.assign(pump, {
runHours: 12640,
starts: 3214,
lastServiceHours: 10800,
lastServiceDate: day(-190),
lastServiceBy: 'J. Novak',
condition: [
{ label: 'Vibration, drive end', value: 4.6, unit: 'mm/s', status: 'warning', limit: 4.5 },
{ label: 'Vibration, non-drive end', value: 2.1, unit: 'mm/s', status: 'normal' },
{ label: 'Bearing temperature', value: 61, unit: '°C', status: 'normal' },
{ label: 'Motor current', value: 38.4, unit: 'A', status: 'normal' }
],
workOrders: orders['P-101'].slice()
});
Object.assign(dosing, {
runHours: 21460,
starts: 18230,
lastServiceHours: 17200,
lastServiceDate: day(-400),
lastServiceBy: 'J. Novak',
condition: [
{ label: 'Discharge pressure', value: 11.8, unit: 'bar', status: 'alarm', limit: 10 },
{ label: 'Stroke rate', value: 0, unit: '/min', status: 'normal' }
],
workOrders: orders['P-204'].slice()
});
/* The run-hour counter ticks on the feed pump while it runs (sped up). */
setInterval(function () {
if (pump.state === 'running') {
pump.runHours += 1;
}
}, 4000);
[pump, dosing].forEach(function (card) {
card.addEventListener('workOrderRequest', function (event) {
const detail = event.detail;
card.busy = true;
note.textContent = 'Sending the request...';
setTimeout(function () {
card.busy = false;
orders[detail.assetId].unshift({ id: 'WO-' + next++, title: detail.text, status: 'open', priority: detail.priority });
card.workOrders = orders[detail.assetId].slice();
note.textContent = 'Work order WO-' + (next - 1) + ' opened on ' + detail.assetId + ' (' + detail.priority + '): "' + detail.text + '"';
}, 500);
});
card.addEventListener('serviceRecord', function (event) {
const detail = event.detail;
card.lastServiceHours = detail.runHours;
card.lastServiceDate = new Date().toISOString();
card.lastServiceBy = 'A. Brandt';
note.textContent = 'Service of ' + detail.assetId + ' recorded at ' + detail.runHours + ' h: the interval starts again.';
});
card.addEventListener('workOrderOpen', function (event) {
note.textContent = 'Open ' + event.detail.id + ' in the maintenance system.';
});
});
}
angular/app.component.css
/* Cards at the width they would have on a maintenance screen. */
.demo-row {
display: flex;
flex-wrap: wrap;
gap: 24px;
align-items: flex-start;
}
smart-asset-card {
width: 380px;
max-width: 100%;
}
.demo-row.wall {
gap: 12px;
}
.demo-row.wall smart-asset-card {
width: 240px;
}
/* ── 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 { AssetCard } from 'smart-industrial/react/assetcard';
/* The demo logic is the HTML demo's: it feeds the elements the way a plant's data would. */
/* The asset card: the page plays the station, which reads the run-hour counter, keeps the work
orders and records a service against the hours it was done at. */
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 pump = document.getElementById('pump'),
dosing = document.getElementById('dosing'),
note = document.getElementById('note'),
day = (days) => new Date(Date.now() + days * 86400000).toISOString(),
orders = {
'P-101': [
{ id: 'WO-1182', title: 'Drive-end bearing noise', status: 'open', priority: 'high', due: day(2) },
{ id: 'WO-1175', title: 'Replace mechanical seal kit', status: 'in-progress', priority: 'normal', due: day(6) }
],
'P-204': [
{ id: 'WO-1190', title: 'Dosing pump tripped on overpressure', status: 'open', priority: 'high', due: day(0) }
]
};
let next = 1191;
Object.assign(pump, {
runHours: 12640,
starts: 3214,
lastServiceHours: 10800,
lastServiceDate: day(-190),
lastServiceBy: 'J. Novak',
condition: [
{ label: 'Vibration, drive end', value: 4.6, unit: 'mm/s', status: 'warning', limit: 4.5 },
{ label: 'Vibration, non-drive end', value: 2.1, unit: 'mm/s', status: 'normal' },
{ label: 'Bearing temperature', value: 61, unit: '°C', status: 'normal' },
{ label: 'Motor current', value: 38.4, unit: 'A', status: 'normal' }
],
workOrders: orders['P-101'].slice()
});
Object.assign(dosing, {
runHours: 21460,
starts: 18230,
lastServiceHours: 17200,
lastServiceDate: day(-400),
lastServiceBy: 'J. Novak',
condition: [
{ label: 'Discharge pressure', value: 11.8, unit: 'bar', status: 'alarm', limit: 10 },
{ label: 'Stroke rate', value: 0, unit: '/min', status: 'normal' }
],
workOrders: orders['P-204'].slice()
});
/* The run-hour counter ticks on the feed pump while it runs (sped up). */
setInterval(function () {
if (pump.state === 'running') {
pump.runHours += 1;
}
}, 4000);
[pump, dosing].forEach(function (card) {
card.addEventListener('workOrderRequest', function (event) {
const detail = event.detail;
card.busy = true;
note.textContent = 'Sending the request...';
setTimeout(function () {
card.busy = false;
orders[detail.assetId].unshift({ id: 'WO-' + next++, title: detail.text, status: 'open', priority: detail.priority });
card.workOrders = orders[detail.assetId].slice();
note.textContent = 'Work order WO-' + (next - 1) + ' opened on ' + detail.assetId + ' (' + detail.priority + '): "' + detail.text + '"';
}, 500);
});
card.addEventListener('serviceRecord', function (event) {
const detail = event.detail;
card.lastServiceHours = detail.runHours;
card.lastServiceDate = new Date().toISOString();
card.lastServiceBy = 'A. Brandt';
note.textContent = 'Service of ' + detail.assetId + ' recorded at ' + detail.runHours + ' h: the interval starts again.';
});
card.addEventListener('workOrderOpen', function (event) {
note.textContent = 'Open ' + event.detail.id + ' in the maintenance system.';
});
});
});
return () => {
unmounted = true;
};
}, []);
return (
<div className="react-root" ref={root} theme="industrial">
<div className="smart-demo-container">
<section id="overview">
<h2>Smart.AssetCard is one machine as maintenance sees it: how long it has run, when it is due, and what is open against it.</h2>
<p>The run hours and starts come from the PLC's counters, the service interval is the maker's, and the condition readings are whatever the plant measures. The card's first job is to answer "is it due" in hours, before anybody opens a maintenance system. Colour is kept for what is abnormal - a service due soon or overdue, a reading out of band, a machine in fault - and never for a machine that is simply running.</p>
</section>
<section>
<h2>Two pumps</h2>
<p>The feed pump is running and due for service soon, with its drive-end vibration above the warning band. The dosing pump has tripped and is past its service. <b>Request work</b> and <b>Record service</b> raise events; the page plays the station that keeps them.</p>
<div className="demo-row">
<AssetCard id="pump" name="Feed pump P-101" assetId="P-101" model="KSB Etanorm 65-200" location="Pump house, bay 2" serviceInterval={2000}></AssetCard>
<AssetCard id="dosing" name="Dosing pump P-204" assetId="P-204" model="Grundfos DDA 7.5" location="Chemical room" serviceInterval={4000} state="fault"></AssetCard>
</div>
<p className="hint" id="note">The card raises <code>workOrderRequest</code>, <code>serviceRecord</code> and <code>workOrderOpen</code>.</p>
</section>
<section>
<h2>Compact, for a wall of assets</h2>
<p><code>compact</code> keeps the name, the state and the service: enough to see, across a line, which machine is next.</p>
<div className="demo-row wall">
<AssetCard compact readOnly name="Infeed conveyor" assetId="CV-301" serviceInterval={3000} runHours={8120} lastServiceHours={6000}></AssetCard>
<AssetCard compact readOnly name="Filler" assetId="FL-310" serviceInterval={1000} runHours={8120} lastServiceHours={7200}></AssetCard>
<AssetCard compact readOnly name="Capper" assetId="CP-320" serviceInterval={1500} runHours={8120} lastServiceHours={6500} state="maintenance"></AssetCard>
<AssetCard compact readOnly name="Labeller" assetId="LB-330" serviceInterval={2500} runHours={8120} lastServiceHours={7000} state="stopped"></AssetCard>
</div>
</section>
</div>
</div>
);
};
export default App;
react/App.css
/* Cards at the width they would have on a maintenance screen. */
.demo-row {
display: flex;
flex-wrap: wrap;
gap: 24px;
align-items: flex-start;
}
smart-asset-card {
width: 380px;
max-width: 100%;
}
.demo-row.wall {
gap: 12px;
}
.demo-row.wall smart-asset-card {
width: 240px;
}
/* ── 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.AssetCard is one machine as maintenance sees it: how long it has run, when it is due, and what is open against it.</h2>
<p>The run hours and starts come from the PLC's counters, the service interval is the maker's, and the
condition readings are whatever the plant measures. The card's first job is to answer "is it due" in hours,
before anybody opens a maintenance system. Colour is kept for what is abnormal - a service due soon or overdue,
a reading out of band, a machine in fault - and never for a machine that is simply running.</p>
</section>
<section>
<h2>Two pumps</h2>
<p>The feed pump is running and due for service soon, with its drive-end vibration above the warning band. The
dosing pump has tripped and is past its service. <b>Request work</b> and <b>Record service</b> raise events;
the page plays the station that keeps them.</p>
<div class="demo-row">
<smart-asset-card id="pump" name="Feed pump P-101" asset-id="P-101" model="KSB Etanorm 65-200" location="Pump house, bay 2" service-interval="2000"></smart-asset-card>
<smart-asset-card id="dosing" name="Dosing pump P-204" asset-id="P-204" model="Grundfos DDA 7.5" location="Chemical room" service-interval="4000" state="fault"></smart-asset-card>
</div>
<p class="hint" id="note">The card raises <code>workOrderRequest</code>, <code>serviceRecord</code> and <code>workOrderOpen</code>.</p>
</section>
<section>
<h2>Compact, for a wall of assets</h2>
<p><code>compact</code> keeps the name, the state and the service: enough to see, across a line, which machine
is next.</p>
<div class="demo-row wall">
<smart-asset-card compact read-only name="Infeed conveyor" asset-id="CV-301" service-interval="3000" run-hours="8120" last-service-hours="6000"></smart-asset-card>
<smart-asset-card compact read-only name="Filler" asset-id="FL-310" service-interval="1000" run-hours="8120" last-service-hours="7200"></smart-asset-card>
<smart-asset-card compact read-only name="Capper" asset-id="CP-320" service-interval="1500" run-hours="8120" last-service-hours="6500" state="maintenance"></smart-asset-card>
<smart-asset-card compact read-only name="Labeller" asset-id="LB-330" service-interval="2500" run-hours="8120" last-service-hours="7000" state="stopped"></smart-asset-card>
</div>
</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.assetcard.js";
/* The asset card: the page plays the station, which reads the run-hour counter, keeps the work
orders and records a service against the hours it was done at. */
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 pump = document.getElementById('pump'),
dosing = document.getElementById('dosing'),
note = document.getElementById('note'),
day = (days) => new Date(Date.now() + days * 86400000).toISOString(),
orders = {
'P-101': [
{ id: 'WO-1182', title: 'Drive-end bearing noise', status: 'open', priority: 'high', due: day(2) },
{ id: 'WO-1175', title: 'Replace mechanical seal kit', status: 'in-progress', priority: 'normal', due: day(6) }
],
'P-204': [
{ id: 'WO-1190', title: 'Dosing pump tripped on overpressure', status: 'open', priority: 'high', due: day(0) }
]
};
let next = 1191;
Object.assign(pump, {
runHours: 12640,
starts: 3214,
lastServiceHours: 10800,
lastServiceDate: day(-190),
lastServiceBy: 'J. Novak',
condition: [
{ label: 'Vibration, drive end', value: 4.6, unit: 'mm/s', status: 'warning', limit: 4.5 },
{ label: 'Vibration, non-drive end', value: 2.1, unit: 'mm/s', status: 'normal' },
{ label: 'Bearing temperature', value: 61, unit: '°C', status: 'normal' },
{ label: 'Motor current', value: 38.4, unit: 'A', status: 'normal' }
],
workOrders: orders['P-101'].slice()
});
Object.assign(dosing, {
runHours: 21460,
starts: 18230,
lastServiceHours: 17200,
lastServiceDate: day(-400),
lastServiceBy: 'J. Novak',
condition: [
{ label: 'Discharge pressure', value: 11.8, unit: 'bar', status: 'alarm', limit: 10 },
{ label: 'Stroke rate', value: 0, unit: '/min', status: 'normal' }
],
workOrders: orders['P-204'].slice()
});
/* The run-hour counter ticks on the feed pump while it runs (sped up). */
setInterval(function () {
if (pump.state === 'running') {
pump.runHours += 1;
}
}, 4000);
[pump, dosing].forEach(function (card) {
card.addEventListener('workOrderRequest', function (event) {
const detail = event.detail;
card.busy = true;
note.textContent = 'Sending the request...';
setTimeout(function () {
card.busy = false;
orders[detail.assetId].unshift({ id: 'WO-' + next++, title: detail.text, status: 'open', priority: detail.priority });
card.workOrders = orders[detail.assetId].slice();
note.textContent = 'Work order WO-' + (next - 1) + ' opened on ' + detail.assetId + ' (' + detail.priority + '): "' + detail.text + '"';
}, 500);
});
card.addEventListener('serviceRecord', function (event) {
const detail = event.detail;
card.lastServiceHours = detail.runHours;
card.lastServiceDate = new Date().toISOString();
card.lastServiceBy = 'A. Brandt';
note.textContent = 'Service of ' + detail.assetId + ' recorded at ' + detail.runHours + ' h: the interval starts again.';
});
card.addEventListener('workOrderOpen', function (event) {
note.textContent = 'Open ' + event.detail.id + ' in the maintenance system.';
});
});
});
});
}
};
</script>
<style>
/* Cards at the width they would have on a maintenance screen. */
.demo-row {
display: flex;
flex-wrap: wrap;
gap: 24px;
align-items: flex-start;
}
smart-asset-card {
width: 380px;
max-width: 100%;
}
.demo-row.wall {
gap: 12px;
}
.demo-row.wall smart-asset-card {
width: 240px;
}
/* ── 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
- Two pumps
- Compact, for a wall of assets