Component demo · Recipes, orders and scheduled jobs
Order Queue
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>Order Queue 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.OrderQueue shows what the line is making, how far along it is, and what comes next.</h2>
<p>After "why did it stop", the question a plant asks of its line is "what is it making, and will it be done
by two". The order on the line comes first, with its count against its quantity and, once it has run long
enough to have a rate, when it will be done at that rate against when it is due.</p>
</section>
<section>
<h2>A line's orders</h2>
<p><b>Start</b> is offered on each queued order and refused, with the reason, while another order is on the
line: a line makes one thing at a time. <b>Complete</b> and <b>Abort</b> ask first, because a finished order
is not started again. The station loads an order's recipe as it starts it; if the plant will not take all
of the recipe, the order does not start.</p>
<div class="demo-row">
<smart-order-queue id="queue" label="Line 3 orders"></smart-order-queue>
</div>
<div class="demo-actions">
<button type="button" id="refuse">Make the station refuse the next start</button>
</div>
<p class="hint" id="note">The queue raises <code>orderAction</code>; this page plays the station that acts on it.</p>
</section>
<section>
<h2>Read-only</h2>
<p><code>readOnly</code>: the same queue on an office screen, without a button on it.</p>
<div class="demo-row">
<smart-order-queue id="viewer" label="Line 3 orders (view only)" read-only></smart-order-queue>
</div>
</section>
</div>
<!-- scripts -->
<script type="module" src="node_modules/smart-industrial/source/modules/smart.orderqueue.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 order queue: the page plays the station, which loads an order's recipe when it starts,
counts it from the line's counter, refuses what makes no sense, and hands the queue back. */
window.onload = function () {
const queue = document.getElementById('queue'),
viewer = document.getElementById('viewer'),
note = document.getElementById('note'),
at = (minutes) => new Date(Date.now() + minutes * 60000).toISOString(),
ORDERS = [
{ id: 'ORD-24117', product: 'PE film 40 µm, 1,200 mm', quantity: 12000, unit: 'pcs', due: at(95), recipe: 'film-40', status: 'running', produced: 7420, started: at(-150) },
{ id: 'ORD-24118', product: 'PE film 60 µm, 1,200 mm', quantity: 8000, unit: 'pcs', due: at(260), recipe: 'film-60', status: 'queued', produced: 0 },
{ id: 'ORD-24120', product: 'Shrink hood 90 µm', quantity: 2500, unit: 'pcs', due: at(420), recipe: 'hood-90', status: 'queued', produced: 0 },
{ id: 'ORD-24121', product: 'PE film 40 µm, 800 mm', quantity: 6000, unit: 'pcs', due: at(600), recipe: 'film-40n', status: 'queued', produced: 0 },
{ id: 'ORD-24109', product: 'PE film 40 µm, 1,200 mm', quantity: 10000, unit: 'pcs', due: at(-200), recipe: 'film-40', status: 'done', produced: 10046, started: at(-420), finished: at(-160) },
{ id: 'ORD-24112', product: 'Stretch film trial', quantity: 500, unit: 'pcs', due: at(-300), recipe: '', status: 'aborted', produced: 120, started: at(-460), finished: at(-440) }
];
let refuseNext = false;
const show = () => {
[queue, viewer].forEach((one) => { one.orders = ORDERS.map((order) => Object.assign({}, order)); });
};
show();
/* The line's counter: about 50 a minute while an order runs. */
setInterval(function () {
const running = ORDERS.find((order) => order.status === 'running');
if (running && running.produced < running.quantity) {
running.produced = Math.min(running.quantity, running.produced + 1);
show();
}
}, 1200);
queue.addEventListener('orderAction', function (event) {
const detail = event.detail,
order = ORDERS.find((one) => one.id === detail.id);
queue.busy = detail.id;
queue.error = '';
note.textContent = detail.action === 'start' && order.recipe ? 'Loading recipe ' + order.recipe + '...' : 'Asking the station...';
setTimeout(function () {
queue.busy = '';
if (refuseNext && detail.action === 'start') {
refuseNext = false;
queue.error = 'The recipe ' + order.recipe + ' was not fully loaded (plc.EXTRUDER.ZONE3.SP), so the order did not start.';
note.textContent = 'Refused: the order stays queued.';
return;
}
if (detail.action === 'start' && ORDERS.some((one) => one.status === 'running' || one.status === 'paused')) {
queue.error = 'Another order is on the line: complete or abort it first.';
note.textContent = 'Refused by the station.';
return;
}
const to = { start: 'running', resume: 'running', pause: 'paused', complete: 'done', abort: 'aborted' }[detail.action];
order.status = to;
if (detail.action === 'start') {
order.started = new Date().toISOString();
}
if (to === 'done' || to === 'aborted') {
order.finished = new Date().toISOString();
}
show();
note.textContent = order.id + ' ' + { start: 'started' + (order.recipe ? ', recipe ' + order.recipe + ' loaded' : ''), resume: 'resumed', pause: 'paused', complete: 'completed at ' + order.produced, abort: 'aborted' }[detail.action] + '.';
}, 600);
});
document.getElementById('refuse').addEventListener('click', function () {
refuseNext = true;
note.textContent = 'The next start will be refused: the plant will not take part of the recipe.';
});
};
styles.css
/* The queue is a real panel on a screen, so it is given the room one would have. */
.demo-row {
display: flex;
flex-wrap: wrap;
gap: 24px;
align-items: flex-start;
}
smart-order-queue {
width: 760px;
max-width: 100%;
}
/* ── 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.OrderQueue shows what the line is making, how far along it is, and what comes next.</h2>
<p>After "why did it stop", the question a plant asks of its line is "what is it making, and will it be done
by two". The order on the line comes first, with its count against its quantity and, once it has run long
enough to have a rate, when it will be done at that rate against when it is due.</p>
</section>
<section>
<h2>A line's orders</h2>
<p><b>Start</b> is offered on each queued order and refused, with the reason, while another order is on the
line: a line makes one thing at a time. <b>Complete</b> and <b>Abort</b> ask first, because a finished order
is not started again. The station loads an order's recipe as it starts it; if the plant will not take all
of the recipe, the order does not start.</p>
<div class="demo-row">
<smart-order-queue id="queue" label="Line 3 orders"></smart-order-queue>
</div>
<div class="demo-actions">
<button type="button" id="refuse">Make the station refuse the next start</button>
</div>
<p class="hint" id="note">The queue raises <code>orderAction</code>; this page plays the station that acts on it.</p>
</section>
<section>
<h2>Read-only</h2>
<p><code>readOnly</code>: the same queue on an office screen, without a button on it.</p>
<div class="demo-row">
<smart-order-queue id="viewer" label="Line 3 orders (view only)" [readOnly]="true"></smart-order-queue>
</div>
</section>
</div>
</div>
angular/app.component.ts
import { Component, AfterViewInit, OnDestroy } from '@angular/core';
import { OrderQueueModule } from 'smart-industrial/angular/orderqueue';
import { runDemo } from './demo';
/**
* Smart.UI Industrial - OrderQueue 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: [OrderQueueModule],
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 order queue: the page plays the station, which loads an order's recipe when it starts,
counts it from the line's counter, refuses what makes no sense, and hands the queue back. */
export function runDemo(): void {
const queue = document.getElementById('queue'),
viewer = document.getElementById('viewer'),
note = document.getElementById('note'),
at = (minutes) => new Date(Date.now() + minutes * 60000).toISOString(),
ORDERS = [
{ id: 'ORD-24117', product: 'PE film 40 µm, 1,200 mm', quantity: 12000, unit: 'pcs', due: at(95), recipe: 'film-40', status: 'running', produced: 7420, started: at(-150) },
{ id: 'ORD-24118', product: 'PE film 60 µm, 1,200 mm', quantity: 8000, unit: 'pcs', due: at(260), recipe: 'film-60', status: 'queued', produced: 0 },
{ id: 'ORD-24120', product: 'Shrink hood 90 µm', quantity: 2500, unit: 'pcs', due: at(420), recipe: 'hood-90', status: 'queued', produced: 0 },
{ id: 'ORD-24121', product: 'PE film 40 µm, 800 mm', quantity: 6000, unit: 'pcs', due: at(600), recipe: 'film-40n', status: 'queued', produced: 0 },
{ id: 'ORD-24109', product: 'PE film 40 µm, 1,200 mm', quantity: 10000, unit: 'pcs', due: at(-200), recipe: 'film-40', status: 'done', produced: 10046, started: at(-420), finished: at(-160) },
{ id: 'ORD-24112', product: 'Stretch film trial', quantity: 500, unit: 'pcs', due: at(-300), recipe: '', status: 'aborted', produced: 120, started: at(-460), finished: at(-440) }
];
let refuseNext = false;
const show = () => {
[queue, viewer].forEach((one) => { one.orders = ORDERS.map((order) => Object.assign({}, order)); });
};
show();
/* The line's counter: about 50 a minute while an order runs. */
setInterval(function () {
const running = ORDERS.find((order) => order.status === 'running');
if (running && running.produced < running.quantity) {
running.produced = Math.min(running.quantity, running.produced + 1);
show();
}
}, 1200);
queue.addEventListener('orderAction', function (event) {
const detail = event.detail,
order = ORDERS.find((one) => one.id === detail.id);
queue.busy = detail.id;
queue.error = '';
note.textContent = detail.action === 'start' && order.recipe ? 'Loading recipe ' + order.recipe + '...' : 'Asking the station...';
setTimeout(function () {
queue.busy = '';
if (refuseNext && detail.action === 'start') {
refuseNext = false;
queue.error = 'The recipe ' + order.recipe + ' was not fully loaded (plc.EXTRUDER.ZONE3.SP), so the order did not start.';
note.textContent = 'Refused: the order stays queued.';
return;
}
if (detail.action === 'start' && ORDERS.some((one) => one.status === 'running' || one.status === 'paused')) {
queue.error = 'Another order is on the line: complete or abort it first.';
note.textContent = 'Refused by the station.';
return;
}
const to = { start: 'running', resume: 'running', pause: 'paused', complete: 'done', abort: 'aborted' }[detail.action];
order.status = to;
if (detail.action === 'start') {
order.started = new Date().toISOString();
}
if (to === 'done' || to === 'aborted') {
order.finished = new Date().toISOString();
}
show();
note.textContent = order.id + ' ' + { start: 'started' + (order.recipe ? ', recipe ' + order.recipe + ' loaded' : ''), resume: 'resumed', pause: 'paused', complete: 'completed at ' + order.produced, abort: 'aborted' }[detail.action] + '.';
}, 600);
});
document.getElementById('refuse').addEventListener('click', function () {
refuseNext = true;
note.textContent = 'The next start will be refused: the plant will not take part of the recipe.';
});
}
angular/app.component.css
/* The queue is a real panel on a screen, so it is given the room one would have. */
.demo-row {
display: flex;
flex-wrap: wrap;
gap: 24px;
align-items: flex-start;
}
smart-order-queue {
width: 760px;
max-width: 100%;
}
/* ── 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 { OrderQueue } from 'smart-industrial/react/orderqueue';
/* The demo logic is the HTML demo's: it feeds the elements the way a plant's data would. */
/* The order queue: the page plays the station, which loads an order's recipe when it starts,
counts it from the line's counter, refuses what makes no sense, and hands the queue back. */
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 queue = document.getElementById('queue'),
viewer = document.getElementById('viewer'),
note = document.getElementById('note'),
at = (minutes) => new Date(Date.now() + minutes * 60000).toISOString(),
ORDERS = [
{ id: 'ORD-24117', product: 'PE film 40 µm, 1,200 mm', quantity: 12000, unit: 'pcs', due: at(95), recipe: 'film-40', status: 'running', produced: 7420, started: at(-150) },
{ id: 'ORD-24118', product: 'PE film 60 µm, 1,200 mm', quantity: 8000, unit: 'pcs', due: at(260), recipe: 'film-60', status: 'queued', produced: 0 },
{ id: 'ORD-24120', product: 'Shrink hood 90 µm', quantity: 2500, unit: 'pcs', due: at(420), recipe: 'hood-90', status: 'queued', produced: 0 },
{ id: 'ORD-24121', product: 'PE film 40 µm, 800 mm', quantity: 6000, unit: 'pcs', due: at(600), recipe: 'film-40n', status: 'queued', produced: 0 },
{ id: 'ORD-24109', product: 'PE film 40 µm, 1,200 mm', quantity: 10000, unit: 'pcs', due: at(-200), recipe: 'film-40', status: 'done', produced: 10046, started: at(-420), finished: at(-160) },
{ id: 'ORD-24112', product: 'Stretch film trial', quantity: 500, unit: 'pcs', due: at(-300), recipe: '', status: 'aborted', produced: 120, started: at(-460), finished: at(-440) }
];
let refuseNext = false;
const show = () => {
[queue, viewer].forEach((one) => { one.orders = ORDERS.map((order) => Object.assign({}, order)); });
};
show();
/* The line's counter: about 50 a minute while an order runs. */
setInterval(function () {
const running = ORDERS.find((order) => order.status === 'running');
if (running && running.produced < running.quantity) {
running.produced = Math.min(running.quantity, running.produced + 1);
show();
}
}, 1200);
queue.addEventListener('orderAction', function (event) {
const detail = event.detail,
order = ORDERS.find((one) => one.id === detail.id);
queue.busy = detail.id;
queue.error = '';
note.textContent = detail.action === 'start' && order.recipe ? 'Loading recipe ' + order.recipe + '...' : 'Asking the station...';
setTimeout(function () {
queue.busy = '';
if (refuseNext && detail.action === 'start') {
refuseNext = false;
queue.error = 'The recipe ' + order.recipe + ' was not fully loaded (plc.EXTRUDER.ZONE3.SP), so the order did not start.';
note.textContent = 'Refused: the order stays queued.';
return;
}
if (detail.action === 'start' && ORDERS.some((one) => one.status === 'running' || one.status === 'paused')) {
queue.error = 'Another order is on the line: complete or abort it first.';
note.textContent = 'Refused by the station.';
return;
}
const to = { start: 'running', resume: 'running', pause: 'paused', complete: 'done', abort: 'aborted' }[detail.action];
order.status = to;
if (detail.action === 'start') {
order.started = new Date().toISOString();
}
if (to === 'done' || to === 'aborted') {
order.finished = new Date().toISOString();
}
show();
note.textContent = order.id + ' ' + { start: 'started' + (order.recipe ? ', recipe ' + order.recipe + ' loaded' : ''), resume: 'resumed', pause: 'paused', complete: 'completed at ' + order.produced, abort: 'aborted' }[detail.action] + '.';
}, 600);
});
document.getElementById('refuse').addEventListener('click', function () {
refuseNext = true;
note.textContent = 'The next start will be refused: the plant will not take part of the recipe.';
});
});
return () => {
unmounted = true;
};
}, []);
return (
<div className="react-root" ref={root} theme="industrial">
<div className="smart-demo-container">
<section id="overview">
<h2>Smart.OrderQueue shows what the line is making, how far along it is, and what comes next.</h2>
<p>After "why did it stop", the question a plant asks of its line is "what is it making, and will it be done by two". The order on the line comes first, with its count against its quantity and, once it has run long enough to have a rate, when it will be done at that rate against when it is due.</p>
</section>
<section>
<h2>A line's orders</h2>
<p><b>Start</b> is offered on each queued order and refused, with the reason, while another order is on the line: a line makes one thing at a time. <b>Complete</b> and <b>Abort</b> ask first, because a finished order is not started again. The station loads an order's recipe as it starts it; if the plant will not take all of the recipe, the order does not start.</p>
<div className="demo-row">
<OrderQueue id="queue" label="Line 3 orders"></OrderQueue>
</div>
<div className="demo-actions">
<button type="button" id="refuse">Make the station refuse the next start</button>
</div>
<p className="hint" id="note">The queue raises <code>orderAction</code>; this page plays the station that acts on it.</p>
</section>
<section>
<h2>Read-only</h2>
<p><code>readOnly</code>: the same queue on an office screen, without a button on it.</p>
<div className="demo-row">
<OrderQueue id="viewer" label="Line 3 orders (view only)" readOnly></OrderQueue>
</div>
</section>
</div>
</div>
);
};
export default App;
react/App.css
/* The queue is a real panel on a screen, so it is given the room one would have. */
.demo-row {
display: flex;
flex-wrap: wrap;
gap: 24px;
align-items: flex-start;
}
smart-order-queue {
width: 760px;
max-width: 100%;
}
/* ── 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.OrderQueue shows what the line is making, how far along it is, and what comes next.</h2>
<p>After "why did it stop", the question a plant asks of its line is "what is it making, and will it be done
by two". The order on the line comes first, with its count against its quantity and, once it has run long
enough to have a rate, when it will be done at that rate against when it is due.</p>
</section>
<section>
<h2>A line's orders</h2>
<p><b>Start</b> is offered on each queued order and refused, with the reason, while another order is on the
line: a line makes one thing at a time. <b>Complete</b> and <b>Abort</b> ask first, because a finished order
is not started again. The station loads an order's recipe as it starts it; if the plant will not take all
of the recipe, the order does not start.</p>
<div class="demo-row">
<smart-order-queue id="queue" label="Line 3 orders"></smart-order-queue>
</div>
<div class="demo-actions">
<button type="button" id="refuse">Make the station refuse the next start</button>
</div>
<p class="hint" id="note">The queue raises <code>orderAction</code>; this page plays the station that acts on it.</p>
</section>
<section>
<h2>Read-only</h2>
<p><code>readOnly</code>: the same queue on an office screen, without a button on it.</p>
<div class="demo-row">
<smart-order-queue id="viewer" label="Line 3 orders (view only)" read-only></smart-order-queue>
</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.orderqueue.js";
/* The order queue: the page plays the station, which loads an order's recipe when it starts,
counts it from the line's counter, refuses what makes no sense, and hands the queue back. */
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 queue = document.getElementById('queue'),
viewer = document.getElementById('viewer'),
note = document.getElementById('note'),
at = (minutes) => new Date(Date.now() + minutes * 60000).toISOString(),
ORDERS = [
{ id: 'ORD-24117', product: 'PE film 40 µm, 1,200 mm', quantity: 12000, unit: 'pcs', due: at(95), recipe: 'film-40', status: 'running', produced: 7420, started: at(-150) },
{ id: 'ORD-24118', product: 'PE film 60 µm, 1,200 mm', quantity: 8000, unit: 'pcs', due: at(260), recipe: 'film-60', status: 'queued', produced: 0 },
{ id: 'ORD-24120', product: 'Shrink hood 90 µm', quantity: 2500, unit: 'pcs', due: at(420), recipe: 'hood-90', status: 'queued', produced: 0 },
{ id: 'ORD-24121', product: 'PE film 40 µm, 800 mm', quantity: 6000, unit: 'pcs', due: at(600), recipe: 'film-40n', status: 'queued', produced: 0 },
{ id: 'ORD-24109', product: 'PE film 40 µm, 1,200 mm', quantity: 10000, unit: 'pcs', due: at(-200), recipe: 'film-40', status: 'done', produced: 10046, started: at(-420), finished: at(-160) },
{ id: 'ORD-24112', product: 'Stretch film trial', quantity: 500, unit: 'pcs', due: at(-300), recipe: '', status: 'aborted', produced: 120, started: at(-460), finished: at(-440) }
];
let refuseNext = false;
const show = () => {
[queue, viewer].forEach((one) => { one.orders = ORDERS.map((order) => Object.assign({}, order)); });
};
show();
/* The line's counter: about 50 a minute while an order runs. */
setInterval(function () {
const running = ORDERS.find((order) => order.status === 'running');
if (running && running.produced < running.quantity) {
running.produced = Math.min(running.quantity, running.produced + 1);
show();
}
}, 1200);
queue.addEventListener('orderAction', function (event) {
const detail = event.detail,
order = ORDERS.find((one) => one.id === detail.id);
queue.busy = detail.id;
queue.error = '';
note.textContent = detail.action === 'start' && order.recipe ? 'Loading recipe ' + order.recipe + '...' : 'Asking the station...';
setTimeout(function () {
queue.busy = '';
if (refuseNext && detail.action === 'start') {
refuseNext = false;
queue.error = 'The recipe ' + order.recipe + ' was not fully loaded (plc.EXTRUDER.ZONE3.SP), so the order did not start.';
note.textContent = 'Refused: the order stays queued.';
return;
}
if (detail.action === 'start' && ORDERS.some((one) => one.status === 'running' || one.status === 'paused')) {
queue.error = 'Another order is on the line: complete or abort it first.';
note.textContent = 'Refused by the station.';
return;
}
const to = { start: 'running', resume: 'running', pause: 'paused', complete: 'done', abort: 'aborted' }[detail.action];
order.status = to;
if (detail.action === 'start') {
order.started = new Date().toISOString();
}
if (to === 'done' || to === 'aborted') {
order.finished = new Date().toISOString();
}
show();
note.textContent = order.id + ' ' + { start: 'started' + (order.recipe ? ', recipe ' + order.recipe + ' loaded' : ''), resume: 'resumed', pause: 'paused', complete: 'completed at ' + order.produced, abort: 'aborted' }[detail.action] + '.';
}, 600);
});
document.getElementById('refuse').addEventListener('click', function () {
refuseNext = true;
note.textContent = 'The next start will be refused: the plant will not take part of the recipe.';
});
});
});
}
};
</script>
<style>
/* The queue is a real panel on a screen, so it is given the room one would have. */
.demo-row {
display: flex;
flex-wrap: wrap;
gap: 24px;
align-items: flex-start;
}
smart-order-queue {
width: 760px;
max-width: 100%;
}
/* ── 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 line's orders
- Read-only