Component demo · Production floor
Stack Light
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>StackLight 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.StackLight displays the tower light of a machine as a column of coloured segments and an optional horn.</h2>
<p>
Green means running, amber means attention, red means stopped, blue is a call for assistance and white is a changeover. The component can be driven by a state word such as <code>running</code>, <code>starved</code> or <code>fault</code>, which the <code>stateMap</code> property translates into segments that are on, off or flashing, or segment by segment when the application mirrors the outputs of the physical tower. The horn is switched on by the application, and pressing it raises the <code>muteRequest</code> event.
</p>
</section>
<section>
<h2>A packaging line</h2>
<div class="demo-line">
<div class="demo-station"><smart-stack-light id="filler" label="Filler" state="running"></smart-stack-light><span>Filler</span></div>
<div class="demo-station"><smart-stack-light id="capper" label="Capper" state="running"></smart-stack-light><span>Capper</span></div>
<div class="demo-station"><smart-stack-light id="labeller" label="Labeller" state="starved"></smart-stack-light><span>Labeller</span></div>
<div class="demo-station"><smart-stack-light id="packer" label="Case packer" state="fault" horn="on"></smart-stack-light><span>Case packer</span></div>
<div class="demo-station"><smart-stack-light id="palletiser" label="Palletiser" state="blocked"></smart-stack-light><span>Palletiser</span></div>
</div>
<div class="demo-actions">
<label>Case packer <select id="packerState">
<option value="running">running</option>
<option value="ready">ready</option>
<option value="warning">warning</option>
<option value="starved">starved</option>
<option value="blocked">blocked</option>
<option value="stopped">stopped</option>
<option value="fault" selected>fault</option>
<option value="call">call</option>
<option value="maintenance">maintenance</option>
<option value="changeover">changeover</option>
<option value="off">off</option>
</select></label>
<button type="button" id="soundHorn">Sound the horn</button>
</div>
<p class="demo-log" id="log">Press the horn on the case packer. The component raises the muteRequest event and the application mutes the horn.</p>
</section>
<section>
<h2>Five segments, a legend and horizontal orientation</h2>
<p>
A tower takes the five standard colours, red, amber, green, blue and white, and further segments in any CSS colour. The legend shows the name and state of each segment; the same text is used as the accessible name. The <code>orientation</code> property lays the tower horizontally for a header strip.
</p>
<div class="demo-row">
<smart-stack-light id="five" label="Line 2" show-labels></smart-stack-light>
<smart-stack-light id="flat" label="Line 2" orientation="horizontal" state="maintenance" horn="muted"></smart-stack-light>
</div>
</section>
<section>
<h2>A custom state map</h2>
<p>
A plant with a different lighting convention replaces the map. In this example <code>starved</code> lights amber and blue together, and a sixth, plant-specific segment shows a quality hold.
</p>
<smart-stack-light id="custom" label="Retort 1" state="starved" show-labels></smart-stack-light>
<p class="demo-log" id="described"></p>
</section>
</div>
<!-- scripts -->
<script type="module" src="node_modules/smart-industrial/source/modules/smart.stacklight.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" />
/* Stack light overview.
*
* The state words come from the machines' controllers; here a host that owns the horn
* answers the light's request, which is the one piece of behaviour the element leaves to
* the plant on purpose. */
window.onload = function () {
const packer = document.getElementById('packer'),
packerState = document.getElementById('packerState'),
soundHorn = document.getElementById('soundHorn'),
log = document.getElementById('log'),
five = document.getElementById('five'),
custom = document.getElementById('custom'),
described = document.getElementById('described');
/* ── The host owns the horn ─────────────────────────────────────────── */
packer.addEventListener('muteRequest', function (event) {
/* The plant would write the horn output here; the light shows the answer. */
packer.horn = 'muted';
log.textContent = event.detail.label + ': horn silenced by the host. The fault is still there until the state changes.';
});
packer.addEventListener('stateChange', function (event) {
if (event.detail.state !== 'fault') {
packer.horn = 'off';
}
log.textContent = packer.describe();
});
packerState.addEventListener('change', function () {
packer.state = packerState.value;
});
soundHorn.addEventListener('click', function () {
packer.horn = 'on';
log.textContent = packer.describe();
});
/* ── Five colours, driven segment by segment ────────────────────────── */
five.segments = [
{ id: 'red', color: 'red', label: 'Stopped', state: 'off' },
{ id: 'amber', color: 'amber', label: 'Attention', state: 'flash' },
{ id: 'green', color: 'green', label: 'Running', state: 'on' },
{ id: 'blue', color: 'blue', label: 'Operator call', state: 'off' },
{ id: 'white', color: 'white', label: 'Changeover', state: 'off' }
];
document.getElementById('flat').segments = ['red', 'amber', 'green', 'blue', 'white'];
/* ── A plant's own convention ───────────────────────────────────────── */
custom.segments = ['red', 'amber', 'green', 'blue', { id: 'hold', color: '#8e44ad', label: 'Quality hold' }];
custom.stateMap = {
'running': { 'green': 'on' },
'starved': { 'amber': 'on', 'blue': 'on' },
'stopped': { 'red': 'on' },
'fault': { 'red': 'fast' },
'hold': { 'hold': 'flash', 'red': 'on' }
};
described.textContent = custom.describe();
let step = 0;
const states = ['starved', 'running', 'hold', 'running', 'fault', 'stopped'];
setInterval(function () {
step = (step + 1) % states.length;
custom.state = states[step];
described.textContent = custom.describe();
}, 3000);
};
styles.css
.smart-demo-container {
max-width: 900px;
}
section {
margin-bottom: 34px;
}
section h2 {
font-size: 16px;
font-weight: 500;
margin: 0 0 8px;
}
section p {
color: var(--smart-text-secondary-color);
margin: 0 0 14px;
max-width: 74ch;
}
code {
background: var(--smart-ui-state-hover);
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
padding: 1px 5px;
}
.demo-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 14px;
}
.demo-actions button {
background: transparent;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
color: var(--smart-text-secondary-color);
cursor: pointer;
font-family: inherit;
font-size: 11px;
letter-spacing: .06em;
padding: 7px 14px;
text-transform: uppercase;
}
.demo-actions button:hover {
background: var(--smart-ui-state-hover);
border-color: var(--smart-ui-state-border-hover);
color: var(--smart-ui-state-color-hover);
}
.demo-log,
.demo-counts {
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
margin: 12px 0 0;
color: var(--smart-text-tertiary-color);
}
smart-root-cause-tree {
display: block;
max-width: 720px;
padding: 6px 0;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
background: var(--smart-industrial-panel-background, var(--smart-surface));
}
.demo-ranked {
color: var(--smart-text-secondary-color);
font-size: 12px;
}
.demo-line {
display: flex;
flex-wrap: wrap;
gap: 22px;
align-items: flex-end;
padding: 16px 18px;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
}
.demo-station {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--smart-text-secondary-color);
}
.demo-row {
display: flex;
flex-wrap: wrap;
gap: 28px;
align-items: flex-start;
}
.demo-actions label {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
}
.demo-actions select {
font: inherit;
padding: 4px 6px;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
background: var(--smart-background);
color: var(--smart-text-color);
}
smart-stack-light {
--smart-stack-segment-width: 36px;
--smart-stack-segment-height: 28px;
}
/* ── 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.StackLight displays the tower light of a machine as a column of coloured segments and an optional horn.</h2>
<p>
Green means running, amber means attention, red means stopped, blue is a call for assistance and white is a changeover. The component can be driven by a state word such as <code>running</code>, <code>starved</code> or <code>fault</code>, which the <code>stateMap</code> property translates into segments that are on, off or flashing, or segment by segment when the application mirrors the outputs of the physical tower. The horn is switched on by the application, and pressing it raises the <code>muteRequest</code> event.
</p>
</section>
<section>
<h2>A packaging line</h2>
<div class="demo-line">
<div class="demo-station"><smart-stack-light id="filler" label="Filler" state="running"></smart-stack-light><span>Filler</span></div>
<div class="demo-station"><smart-stack-light id="capper" label="Capper" state="running"></smart-stack-light><span>Capper</span></div>
<div class="demo-station"><smart-stack-light id="labeller" label="Labeller" state="starved"></smart-stack-light><span>Labeller</span></div>
<div class="demo-station"><smart-stack-light id="packer" label="Case packer" state="fault" horn="on"></smart-stack-light><span>Case packer</span></div>
<div class="demo-station"><smart-stack-light id="palletiser" label="Palletiser" state="blocked"></smart-stack-light><span>Palletiser</span></div>
</div>
<div class="demo-actions">
<label>Case packer <select id="packerState">
<option value="running">running</option>
<option value="ready">ready</option>
<option value="warning">warning</option>
<option value="starved">starved</option>
<option value="blocked">blocked</option>
<option value="stopped">stopped</option>
<option value="fault" selected>fault</option>
<option value="call">call</option>
<option value="maintenance">maintenance</option>
<option value="changeover">changeover</option>
<option value="off">off</option>
</select></label>
<button type="button" id="soundHorn">Sound the horn</button>
</div>
<p class="demo-log" id="log">Press the horn on the case packer. The component raises the muteRequest event and the application mutes the horn.</p>
</section>
<section>
<h2>Five segments, a legend and horizontal orientation</h2>
<p>
A tower takes the five standard colours, red, amber, green, blue and white, and further segments in any CSS colour. The legend shows the name and state of each segment; the same text is used as the accessible name. The <code>orientation</code> property lays the tower horizontally for a header strip.
</p>
<div class="demo-row">
<smart-stack-light id="five" label="Line 2" [showLabels]="true"></smart-stack-light>
<smart-stack-light id="flat" label="Line 2" orientation="horizontal" state="maintenance" horn="muted"></smart-stack-light>
</div>
</section>
<section>
<h2>A custom state map</h2>
<p>
A plant with a different lighting convention replaces the map. In this example <code>starved</code> lights amber and blue together, and a sixth, plant-specific segment shows a quality hold.
</p>
<smart-stack-light id="custom" label="Retort 1" state="starved" [showLabels]="true"></smart-stack-light>
<p class="demo-log" id="described"></p>
</section>
</div>
</div>
angular/app.component.ts
import { Component, AfterViewInit, OnDestroy } from '@angular/core';
import { StackLightModule } from 'smart-industrial/angular/stacklight';
import { runDemo } from './demo';
/**
* Smart.UI Industrial - StackLight 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: [StackLightModule],
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. */
/* Stack light overview.
*
* The state words come from the machines' controllers; here a host that owns the horn
* answers the light's request, which is the one piece of behaviour the element leaves to
* the plant on purpose. */
export function runDemo(): void {
const packer = document.getElementById('packer'),
packerState = document.getElementById('packerState'),
soundHorn = document.getElementById('soundHorn'),
log = document.getElementById('log'),
five = document.getElementById('five'),
custom = document.getElementById('custom'),
described = document.getElementById('described');
/* ── The host owns the horn ─────────────────────────────────────────── */
packer.addEventListener('muteRequest', function (event) {
/* The plant would write the horn output here; the light shows the answer. */
packer.horn = 'muted';
log.textContent = event.detail.label + ': horn silenced by the host. The fault is still there until the state changes.';
});
packer.addEventListener('stateChange', function (event) {
if (event.detail.state !== 'fault') {
packer.horn = 'off';
}
log.textContent = packer.describe();
});
packerState.addEventListener('change', function () {
packer.state = packerState.value;
});
soundHorn.addEventListener('click', function () {
packer.horn = 'on';
log.textContent = packer.describe();
});
/* ── Five colours, driven segment by segment ────────────────────────── */
five.segments = [
{ id: 'red', color: 'red', label: 'Stopped', state: 'off' },
{ id: 'amber', color: 'amber', label: 'Attention', state: 'flash' },
{ id: 'green', color: 'green', label: 'Running', state: 'on' },
{ id: 'blue', color: 'blue', label: 'Operator call', state: 'off' },
{ id: 'white', color: 'white', label: 'Changeover', state: 'off' }
];
document.getElementById('flat').segments = ['red', 'amber', 'green', 'blue', 'white'];
/* ── A plant's own convention ───────────────────────────────────────── */
custom.segments = ['red', 'amber', 'green', 'blue', { id: 'hold', color: '#8e44ad', label: 'Quality hold' }];
custom.stateMap = {
'running': { 'green': 'on' },
'starved': { 'amber': 'on', 'blue': 'on' },
'stopped': { 'red': 'on' },
'fault': { 'red': 'fast' },
'hold': { 'hold': 'flash', 'red': 'on' }
};
described.textContent = custom.describe();
let step = 0;
const states = ['starved', 'running', 'hold', 'running', 'fault', 'stopped'];
setInterval(function () {
step = (step + 1) % states.length;
custom.state = states[step];
described.textContent = custom.describe();
}, 3000);
}
angular/app.component.css
.smart-demo-container {
max-width: 900px;
}
section {
margin-bottom: 34px;
}
section h2 {
font-size: 16px;
font-weight: 500;
margin: 0 0 8px;
}
section p {
color: var(--smart-text-secondary-color);
margin: 0 0 14px;
max-width: 74ch;
}
code {
background: var(--smart-ui-state-hover);
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
padding: 1px 5px;
}
.demo-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 14px;
}
.demo-actions button {
background: transparent;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
color: var(--smart-text-secondary-color);
cursor: pointer;
font-family: inherit;
font-size: 11px;
letter-spacing: .06em;
padding: 7px 14px;
text-transform: uppercase;
}
.demo-actions button:hover {
background: var(--smart-ui-state-hover);
border-color: var(--smart-ui-state-border-hover);
color: var(--smart-ui-state-color-hover);
}
.demo-log,
.demo-counts {
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
margin: 12px 0 0;
color: var(--smart-text-tertiary-color);
}
smart-root-cause-tree {
display: block;
max-width: 720px;
padding: 6px 0;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
background: var(--smart-industrial-panel-background, var(--smart-surface));
}
.demo-ranked {
color: var(--smart-text-secondary-color);
font-size: 12px;
}
.demo-line {
display: flex;
flex-wrap: wrap;
gap: 22px;
align-items: flex-end;
padding: 16px 18px;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
}
.demo-station {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--smart-text-secondary-color);
}
.demo-row {
display: flex;
flex-wrap: wrap;
gap: 28px;
align-items: flex-start;
}
.demo-actions label {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
}
.demo-actions select {
font: inherit;
padding: 4px 6px;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
background: var(--smart-background);
color: var(--smart-text-color);
}
smart-stack-light {
--smart-stack-segment-width: 36px;
--smart-stack-segment-height: 28px;
}
/* ── 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 { StackLight } from 'smart-industrial/react/stacklight';
/* The demo logic is the HTML demo's: it feeds the elements the way a plant's data would. */
/* Stack light overview.
*
* The state words come from the machines' controllers; here a host that owns the horn
* answers the light's request, which is the one piece of behaviour the element leaves to
* the plant on purpose. */
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 packer = document.getElementById('packer'),
packerState = document.getElementById('packerState'),
soundHorn = document.getElementById('soundHorn'),
log = document.getElementById('log'),
five = document.getElementById('five'),
custom = document.getElementById('custom'),
described = document.getElementById('described');
/* ── The host owns the horn ─────────────────────────────────────────── */
packer.addEventListener('muteRequest', function (event) {
/* The plant would write the horn output here; the light shows the answer. */
packer.horn = 'muted';
log.textContent = event.detail.label + ': horn silenced by the host. The fault is still there until the state changes.';
});
packer.addEventListener('stateChange', function (event) {
if (event.detail.state !== 'fault') {
packer.horn = 'off';
}
log.textContent = packer.describe();
});
packerState.addEventListener('change', function () {
packer.state = packerState.value;
});
soundHorn.addEventListener('click', function () {
packer.horn = 'on';
log.textContent = packer.describe();
});
/* ── Five colours, driven segment by segment ────────────────────────── */
five.segments = [
{ id: 'red', color: 'red', label: 'Stopped', state: 'off' },
{ id: 'amber', color: 'amber', label: 'Attention', state: 'flash' },
{ id: 'green', color: 'green', label: 'Running', state: 'on' },
{ id: 'blue', color: 'blue', label: 'Operator call', state: 'off' },
{ id: 'white', color: 'white', label: 'Changeover', state: 'off' }
];
document.getElementById('flat').segments = ['red', 'amber', 'green', 'blue', 'white'];
/* ── A plant's own convention ───────────────────────────────────────── */
custom.segments = ['red', 'amber', 'green', 'blue', { id: 'hold', color: '#8e44ad', label: 'Quality hold' }];
custom.stateMap = {
'running': { 'green': 'on' },
'starved': { 'amber': 'on', 'blue': 'on' },
'stopped': { 'red': 'on' },
'fault': { 'red': 'fast' },
'hold': { 'hold': 'flash', 'red': 'on' }
};
described.textContent = custom.describe();
let step = 0;
const states = ['starved', 'running', 'hold', 'running', 'fault', 'stopped'];
setInterval(function () {
step = (step + 1) % states.length;
custom.state = states[step];
described.textContent = custom.describe();
}, 3000);
});
return () => {
unmounted = true;
};
}, []);
return (
<div className="react-root" ref={root} theme="industrial">
<div className="smart-demo-container">
<section id="overview">
<h2>Smart.StackLight displays the tower light of a machine as a column of coloured segments and an optional horn.</h2>
<p>Green means running, amber means attention, red means stopped, blue is a call for assistance and white is a changeover. The component can be driven by a state word such as <code>running</code>, <code>starved</code> or <code>fault</code>, which the <code>stateMap</code> property translates into segments that are on, off or flashing, or segment by segment when the application mirrors the outputs of the physical tower. The horn is switched on by the application, and pressing it raises the <code>muteRequest</code> event.</p>
</section>
<section>
<h2>A packaging line</h2>
<div className="demo-line">
<div className="demo-station">
<StackLight id="filler" label="Filler" state="running"></StackLight>
<span>Filler</span>
</div>
<div className="demo-station">
<StackLight id="capper" label="Capper" state="running"></StackLight>
<span>Capper</span>
</div>
<div className="demo-station">
<StackLight id="labeller" label="Labeller" state="starved"></StackLight>
<span>Labeller</span>
</div>
<div className="demo-station">
<StackLight id="packer" label="Case packer" state="fault" horn="on"></StackLight>
<span>Case packer</span>
</div>
<div className="demo-station">
<StackLight id="palletiser" label="Palletiser" state="blocked"></StackLight>
<span>Palletiser</span>
</div>
</div>
<div className="demo-actions">
<label>
Case packer
<select id="packerState" defaultValue="fault"><option value="running">running</option> <option value="ready">ready</option> <option value="warning">warning</option> <option value="starved">starved</option> <option value="blocked">blocked</option> <option value="stopped">stopped</option> <option value="fault">fault</option> <option value="call">call</option> <option value="maintenance">maintenance</option> <option value="changeover">changeover</option> <option value="off">off</option></select>
</label>
<button type="button" id="soundHorn">Sound the horn</button>
</div>
<p className="demo-log" id="log">Press the horn on the case packer. The component raises the muteRequest event and the application mutes the horn.</p>
</section>
<section>
<h2>Five segments, a legend and horizontal orientation</h2>
<p>A tower takes the five standard colours, red, amber, green, blue and white, and further segments in any CSS colour. The legend shows the name and state of each segment; the same text is used as the accessible name. The <code>orientation</code> property lays the tower horizontally for a header strip.</p>
<div className="demo-row">
<StackLight id="five" label="Line 2" showLabels></StackLight>
<StackLight id="flat" label="Line 2" orientation="horizontal" state="maintenance" horn="muted"></StackLight>
</div>
</section>
<section>
<h2>A custom state map</h2>
<p>A plant with a different lighting convention replaces the map. In this example <code>starved</code> lights amber and blue together, and a sixth, plant-specific segment shows a quality hold.</p>
<StackLight id="custom" label="Retort 1" state="starved" showLabels></StackLight>
<p className="demo-log" id="described"></p>
</section>
</div>
</div>
);
};
export default App;
react/App.css
.smart-demo-container {
max-width: 900px;
}
section {
margin-bottom: 34px;
}
section h2 {
font-size: 16px;
font-weight: 500;
margin: 0 0 8px;
}
section p {
color: var(--smart-text-secondary-color);
margin: 0 0 14px;
max-width: 74ch;
}
code {
background: var(--smart-ui-state-hover);
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
padding: 1px 5px;
}
.demo-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 14px;
}
.demo-actions button {
background: transparent;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
color: var(--smart-text-secondary-color);
cursor: pointer;
font-family: inherit;
font-size: 11px;
letter-spacing: .06em;
padding: 7px 14px;
text-transform: uppercase;
}
.demo-actions button:hover {
background: var(--smart-ui-state-hover);
border-color: var(--smart-ui-state-border-hover);
color: var(--smart-ui-state-color-hover);
}
.demo-log,
.demo-counts {
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
margin: 12px 0 0;
color: var(--smart-text-tertiary-color);
}
smart-root-cause-tree {
display: block;
max-width: 720px;
padding: 6px 0;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
background: var(--smart-industrial-panel-background, var(--smart-surface));
}
.demo-ranked {
color: var(--smart-text-secondary-color);
font-size: 12px;
}
.demo-line {
display: flex;
flex-wrap: wrap;
gap: 22px;
align-items: flex-end;
padding: 16px 18px;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
}
.demo-station {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--smart-text-secondary-color);
}
.demo-row {
display: flex;
flex-wrap: wrap;
gap: 28px;
align-items: flex-start;
}
.demo-actions label {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
}
.demo-actions select {
font: inherit;
padding: 4px 6px;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
background: var(--smart-background);
color: var(--smart-text-color);
}
smart-stack-light {
--smart-stack-segment-width: 36px;
--smart-stack-segment-height: 28px;
}
/* ── 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.StackLight displays the tower light of a machine as a column of coloured segments and an optional horn.</h2>
<p>
Green means running, amber means attention, red means stopped, blue is a call for assistance and white is a changeover. The component can be driven by a state word such as <code>running</code>, <code>starved</code> or <code>fault</code>, which the <code>stateMap</code> property translates into segments that are on, off or flashing, or segment by segment when the application mirrors the outputs of the physical tower. The horn is switched on by the application, and pressing it raises the <code>muteRequest</code> event.
</p>
</section>
<section>
<h2>A packaging line</h2>
<div class="demo-line">
<div class="demo-station"><smart-stack-light id="filler" label="Filler" state="running"></smart-stack-light><span>Filler</span></div>
<div class="demo-station"><smart-stack-light id="capper" label="Capper" state="running"></smart-stack-light><span>Capper</span></div>
<div class="demo-station"><smart-stack-light id="labeller" label="Labeller" state="starved"></smart-stack-light><span>Labeller</span></div>
<div class="demo-station"><smart-stack-light id="packer" label="Case packer" state="fault" horn="on"></smart-stack-light><span>Case packer</span></div>
<div class="demo-station"><smart-stack-light id="palletiser" label="Palletiser" state="blocked"></smart-stack-light><span>Palletiser</span></div>
</div>
<div class="demo-actions">
<label>Case packer <select id="packerState">
<option value="running">running</option>
<option value="ready">ready</option>
<option value="warning">warning</option>
<option value="starved">starved</option>
<option value="blocked">blocked</option>
<option value="stopped">stopped</option>
<option value="fault" selected>fault</option>
<option value="call">call</option>
<option value="maintenance">maintenance</option>
<option value="changeover">changeover</option>
<option value="off">off</option>
</select></label>
<button type="button" id="soundHorn">Sound the horn</button>
</div>
<p class="demo-log" id="log">Press the horn on the case packer. The component raises the muteRequest event and the application mutes the horn.</p>
</section>
<section>
<h2>Five segments, a legend and horizontal orientation</h2>
<p>
A tower takes the five standard colours, red, amber, green, blue and white, and further segments in any CSS colour. The legend shows the name and state of each segment; the same text is used as the accessible name. The <code>orientation</code> property lays the tower horizontally for a header strip.
</p>
<div class="demo-row">
<smart-stack-light id="five" label="Line 2" show-labels></smart-stack-light>
<smart-stack-light id="flat" label="Line 2" orientation="horizontal" state="maintenance" horn="muted"></smart-stack-light>
</div>
</section>
<section>
<h2>A custom state map</h2>
<p>
A plant with a different lighting convention replaces the map. In this example <code>starved</code> lights amber and blue together, and a sixth, plant-specific segment shows a quality hold.
</p>
<smart-stack-light id="custom" label="Retort 1" state="starved" show-labels></smart-stack-light>
<p class="demo-log" id="described"></p>
</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.stacklight.js";
/* Stack light overview.
*
* The state words come from the machines' controllers; here a host that owns the horn
* answers the light's request, which is the one piece of behaviour the element leaves to
* the plant on purpose. */
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 packer = document.getElementById('packer'),
packerState = document.getElementById('packerState'),
soundHorn = document.getElementById('soundHorn'),
log = document.getElementById('log'),
five = document.getElementById('five'),
custom = document.getElementById('custom'),
described = document.getElementById('described');
/* ── The host owns the horn ─────────────────────────────────────────── */
packer.addEventListener('muteRequest', function (event) {
/* The plant would write the horn output here; the light shows the answer. */
packer.horn = 'muted';
log.textContent = event.detail.label + ': horn silenced by the host. The fault is still there until the state changes.';
});
packer.addEventListener('stateChange', function (event) {
if (event.detail.state !== 'fault') {
packer.horn = 'off';
}
log.textContent = packer.describe();
});
packerState.addEventListener('change', function () {
packer.state = packerState.value;
});
soundHorn.addEventListener('click', function () {
packer.horn = 'on';
log.textContent = packer.describe();
});
/* ── Five colours, driven segment by segment ────────────────────────── */
five.segments = [
{ id: 'red', color: 'red', label: 'Stopped', state: 'off' },
{ id: 'amber', color: 'amber', label: 'Attention', state: 'flash' },
{ id: 'green', color: 'green', label: 'Running', state: 'on' },
{ id: 'blue', color: 'blue', label: 'Operator call', state: 'off' },
{ id: 'white', color: 'white', label: 'Changeover', state: 'off' }
];
document.getElementById('flat').segments = ['red', 'amber', 'green', 'blue', 'white'];
/* ── A plant's own convention ───────────────────────────────────────── */
custom.segments = ['red', 'amber', 'green', 'blue', { id: 'hold', color: '#8e44ad', label: 'Quality hold' }];
custom.stateMap = {
'running': { 'green': 'on' },
'starved': { 'amber': 'on', 'blue': 'on' },
'stopped': { 'red': 'on' },
'fault': { 'red': 'fast' },
'hold': { 'hold': 'flash', 'red': 'on' }
};
described.textContent = custom.describe();
let step = 0;
const states = ['starved', 'running', 'hold', 'running', 'fault', 'stopped'];
setInterval(function () {
step = (step + 1) % states.length;
custom.state = states[step];
described.textContent = custom.describe();
}, 3000);
});
});
}
};
</script>
<style>
.smart-demo-container {
max-width: 900px;
}
section {
margin-bottom: 34px;
}
section h2 {
font-size: 16px;
font-weight: 500;
margin: 0 0 8px;
}
section p {
color: var(--smart-text-secondary-color);
margin: 0 0 14px;
max-width: 74ch;
}
code {
background: var(--smart-ui-state-hover);
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
padding: 1px 5px;
}
.demo-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 14px;
}
.demo-actions button {
background: transparent;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
color: var(--smart-text-secondary-color);
cursor: pointer;
font-family: inherit;
font-size: 11px;
letter-spacing: .06em;
padding: 7px 14px;
text-transform: uppercase;
}
.demo-actions button:hover {
background: var(--smart-ui-state-hover);
border-color: var(--smart-ui-state-border-hover);
color: var(--smart-ui-state-color-hover);
}
.demo-log,
.demo-counts {
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
margin: 12px 0 0;
color: var(--smart-text-tertiary-color);
}
smart-root-cause-tree {
display: block;
max-width: 720px;
padding: 6px 0;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
background: var(--smart-industrial-panel-background, var(--smart-surface));
}
.demo-ranked {
color: var(--smart-text-secondary-color);
font-size: 12px;
}
.demo-line {
display: flex;
flex-wrap: wrap;
gap: 22px;
align-items: flex-end;
padding: 16px 18px;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
}
.demo-station {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--smart-text-secondary-color);
}
.demo-row {
display: flex;
flex-wrap: wrap;
gap: 28px;
align-items: flex-start;
}
.demo-actions label {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
}
.demo-actions select {
font: inherit;
padding: 4px 6px;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
background: var(--smart-background);
color: var(--smart-text-color);
}
smart-stack-light {
--smart-stack-segment-width: 36px;
--smart-stack-segment-height: 28px;
}
/* ── 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 packaging line
- Five segments, a legend and horizontal orientation
- A custom state map