Component demo · Production floor
Shift Log
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>Shift Log 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.ShiftLog is the book on the control-room desk, kept where the screens are.</h2>
<p>The next shift needs to know what this one knows, and no tag says it: that the filler jammed twice
and was cleared by hand, that maintenance is coming at ten, that the new film roll is thinner than the
last. Kept on the station, the log cannot be lost with the notebook, it can be searched, and it names
who wrote every line.</p>
</section>
<section>
<h2>A shift's log</h2>
<p>Handover entries carry the accent, safety entries the warning edge; everything else is text, as a log
should be. <b>Ctrl+Enter</b> adds an entry; Enter alone starts a new line. Entries from before this shift
sit under <i>Earlier shifts</i>.</p>
<div class="demo-row">
<smart-shift-log id="log" label="Line 3 shift log" author="A. Brandt"></smart-shift-log>
</div>
<div class="demo-actions">
<button type="button" id="refuse">Make the station refuse the next entry</button>
</div>
<p class="hint" id="note">The log raises <code>entryAdd</code>; this page plays the station that stores it.</p>
</section>
<section>
<h2>Corrections, not edits</h2>
<p>An entry is never changed. <b>Correct</b> starts a new entry that says which one it corrects, and the
original stays, marked as corrected. A log that can be rewritten afterwards is a record of what somebody
later wished had been known, not of what was known when - which is also what a regulated site needs.</p>
<p>If the station refuses an entry (it sets <code>error</code>), the draft comes back into the box rather
than being lost.</p>
</section>
<section>
<h2>Read-only</h2>
<p><code>readOnly</code> removes the box and the Correct buttons: a supervisor's view of the same log.</p>
<div class="demo-row">
<smart-shift-log id="viewer" label="Line 3 shift log (view only)" read-only></smart-shift-log>
</div>
</section>
</div>
<!-- scripts -->
<script type="module" src="node_modules/smart-industrial/source/modules/smart.shiftlog.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 shift log: the page plays the station, which stores each entry with its own clock and
the writer's name, and hands the log back. */
window.onload = function () {
const log = document.getElementById('log'),
viewer = document.getElementById('viewer'),
note = document.getElementById('note'),
/* a shift two and a half hours in, whatever the time of day, so its entries are all behind us */
shiftStart = new Date(Date.now() - 150 * 60000),
shiftEnd = new Date(shiftStart.getTime() + 8 * 3600000),
at = (minutes) => new Date(shiftStart.getTime() + minutes * 60000).toISOString(),
ENTRIES = [
{ id: 'e1', at: at(-50), author: 'M. Keller', category: 'handover', text: 'Line running on product B. Filler 2 lost its bottle guide screw an hour before the end of the shift - temporary fix in place, part ordered. Keep an eye on it.', tag: 'plc.FILLER2' },
{ id: 'e2', at: at(-120), author: 'M. Keller', category: 'quality', text: 'Two cartons from lot 4471 rejected for underfill. Checkweigher recalibrated, OK since.' },
{ id: 'e3', at: at(12), author: 'A. Brandt', category: 'note', text: 'Took over from M. Keller. Filler 2 guide checked - holding.' },
{ id: 'e4', at: at(48), author: 'A. Brandt', category: 'production', text: 'Changeover to PE film 40 µm done; first good film seven minutes after the start.' },
{ id: 'e5', at: at(85), author: 'A. Brandt', category: 'safety', text: 'Guard door on the palletiser did not latch first time. Latched on the second try; maintenance told.', tag: 'plc.PAL.DOOR' },
{ id: 'e6', at: at(91), author: 'A. Brandt', category: 'maintenance', text: 'Maintenance coming within the hour for the palletiser door switch. Expect 20 minutes of stop.' }
];
let refuseNext = false;
const show = () => {
[log, viewer].forEach((one) => {
one.shift = { name: 'Shift A', from: shiftStart.toISOString(), to: shiftEnd.toISOString() };
one.entries = ENTRIES.map((entry) => Object.assign({}, entry));
});
};
show();
/* The station stores it - with its own clock and the name of whoever is signed in - and
hands the log back. The element wrote nothing. */
log.addEventListener('entryAdd', function (event) {
const entry = event.detail;
log.busy = true;
note.textContent = 'Storing the entry...';
setTimeout(function () {
log.busy = false;
if (refuseNext) {
refuseNext = false;
log.error = 'The station refused the entry: the session expired. Sign in again - the text is still in the box.';
note.textContent = 'Refused: the draft is back in the box.';
return;
}
ENTRIES.push({ id: 'e' + (ENTRIES.length + 1), at: new Date().toISOString(), author: 'A. Brandt', category: entry.category, text: entry.text, tag: entry.tag, correcting: entry.correcting });
show();
note.textContent = 'Stored: "' + entry.text.slice(0, 60) + (entry.text.length > 60 ? '…' : '') + '"' + (entry.correcting ? ', correcting an earlier entry.' : '.');
}, 600);
});
document.getElementById('refuse').addEventListener('click', function () {
refuseNext = true;
note.textContent = 'The next entry will be refused.';
});
};
styles.css
/* The panel is a real control 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-shift-log {
width: 720px;
max-width: 100%;
height: 620px;
}
/* ── 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.ShiftLog is the book on the control-room desk, kept where the screens are.</h2>
<p>The next shift needs to know what this one knows, and no tag says it: that the filler jammed twice
and was cleared by hand, that maintenance is coming at ten, that the new film roll is thinner than the
last. Kept on the station, the log cannot be lost with the notebook, it can be searched, and it names
who wrote every line.</p>
</section>
<section>
<h2>A shift's log</h2>
<p>Handover entries carry the accent, safety entries the warning edge; everything else is text, as a log
should be. <b>Ctrl+Enter</b> adds an entry; Enter alone starts a new line. Entries from before this shift
sit under <i>Earlier shifts</i>.</p>
<div class="demo-row">
<smart-shift-log id="log" label="Line 3 shift log" author="A. Brandt"></smart-shift-log>
</div>
<div class="demo-actions">
<button type="button" id="refuse">Make the station refuse the next entry</button>
</div>
<p class="hint" id="note">The log raises <code>entryAdd</code>; this page plays the station that stores it.</p>
</section>
<section>
<h2>Corrections, not edits</h2>
<p>An entry is never changed. <b>Correct</b> starts a new entry that says which one it corrects, and the
original stays, marked as corrected. A log that can be rewritten afterwards is a record of what somebody
later wished had been known, not of what was known when - which is also what a regulated site needs.</p>
<p>If the station refuses an entry (it sets <code>error</code>), the draft comes back into the box rather
than being lost.</p>
</section>
<section>
<h2>Read-only</h2>
<p><code>readOnly</code> removes the box and the Correct buttons: a supervisor's view of the same log.</p>
<div class="demo-row">
<smart-shift-log id="viewer" label="Line 3 shift log (view only)" [readOnly]="true"></smart-shift-log>
</div>
</section>
</div>
</div>
angular/app.component.ts
import { Component, AfterViewInit, OnDestroy } from '@angular/core';
import { ShiftLogModule } from 'smart-industrial/angular/shiftlog';
import { runDemo } from './demo';
/**
* Smart.UI Industrial - ShiftLog 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: [ShiftLogModule],
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 shift log: the page plays the station, which stores each entry with its own clock and
the writer's name, and hands the log back. */
export function runDemo(): void {
const log = document.getElementById('log'),
viewer = document.getElementById('viewer'),
note = document.getElementById('note'),
/* a shift two and a half hours in, whatever the time of day, so its entries are all behind us */
shiftStart = new Date(Date.now() - 150 * 60000),
shiftEnd = new Date(shiftStart.getTime() + 8 * 3600000),
at = (minutes) => new Date(shiftStart.getTime() + minutes * 60000).toISOString(),
ENTRIES = [
{ id: 'e1', at: at(-50), author: 'M. Keller', category: 'handover', text: 'Line running on product B. Filler 2 lost its bottle guide screw an hour before the end of the shift - temporary fix in place, part ordered. Keep an eye on it.', tag: 'plc.FILLER2' },
{ id: 'e2', at: at(-120), author: 'M. Keller', category: 'quality', text: 'Two cartons from lot 4471 rejected for underfill. Checkweigher recalibrated, OK since.' },
{ id: 'e3', at: at(12), author: 'A. Brandt', category: 'note', text: 'Took over from M. Keller. Filler 2 guide checked - holding.' },
{ id: 'e4', at: at(48), author: 'A. Brandt', category: 'production', text: 'Changeover to PE film 40 µm done; first good film seven minutes after the start.' },
{ id: 'e5', at: at(85), author: 'A. Brandt', category: 'safety', text: 'Guard door on the palletiser did not latch first time. Latched on the second try; maintenance told.', tag: 'plc.PAL.DOOR' },
{ id: 'e6', at: at(91), author: 'A. Brandt', category: 'maintenance', text: 'Maintenance coming within the hour for the palletiser door switch. Expect 20 minutes of stop.' }
];
let refuseNext = false;
const show = () => {
[log, viewer].forEach((one) => {
one.shift = { name: 'Shift A', from: shiftStart.toISOString(), to: shiftEnd.toISOString() };
one.entries = ENTRIES.map((entry) => Object.assign({}, entry));
});
};
show();
/* The station stores it - with its own clock and the name of whoever is signed in - and
hands the log back. The element wrote nothing. */
log.addEventListener('entryAdd', function (event) {
const entry = event.detail;
log.busy = true;
note.textContent = 'Storing the entry...';
setTimeout(function () {
log.busy = false;
if (refuseNext) {
refuseNext = false;
log.error = 'The station refused the entry: the session expired. Sign in again - the text is still in the box.';
note.textContent = 'Refused: the draft is back in the box.';
return;
}
ENTRIES.push({ id: 'e' + (ENTRIES.length + 1), at: new Date().toISOString(), author: 'A. Brandt', category: entry.category, text: entry.text, tag: entry.tag, correcting: entry.correcting });
show();
note.textContent = 'Stored: "' + entry.text.slice(0, 60) + (entry.text.length > 60 ? '…' : '') + '"' + (entry.correcting ? ', correcting an earlier entry.' : '.');
}, 600);
});
document.getElementById('refuse').addEventListener('click', function () {
refuseNext = true;
note.textContent = 'The next entry will be refused.';
});
}
angular/app.component.css
/* The panel is a real control 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-shift-log {
width: 720px;
max-width: 100%;
height: 620px;
}
/* ── 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 { ShiftLog } from 'smart-industrial/react/shiftlog';
/* The demo logic is the HTML demo's: it feeds the elements the way a plant's data would. */
/* The shift log: the page plays the station, which stores each entry with its own clock and
the writer's name, and hands the log 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 log = document.getElementById('log'),
viewer = document.getElementById('viewer'),
note = document.getElementById('note'),
/* a shift two and a half hours in, whatever the time of day, so its entries are all behind us */
shiftStart = new Date(Date.now() - 150 * 60000),
shiftEnd = new Date(shiftStart.getTime() + 8 * 3600000),
at = (minutes) => new Date(shiftStart.getTime() + minutes * 60000).toISOString(),
ENTRIES = [
{ id: 'e1', at: at(-50), author: 'M. Keller', category: 'handover', text: 'Line running on product B. Filler 2 lost its bottle guide screw an hour before the end of the shift - temporary fix in place, part ordered. Keep an eye on it.', tag: 'plc.FILLER2' },
{ id: 'e2', at: at(-120), author: 'M. Keller', category: 'quality', text: 'Two cartons from lot 4471 rejected for underfill. Checkweigher recalibrated, OK since.' },
{ id: 'e3', at: at(12), author: 'A. Brandt', category: 'note', text: 'Took over from M. Keller. Filler 2 guide checked - holding.' },
{ id: 'e4', at: at(48), author: 'A. Brandt', category: 'production', text: 'Changeover to PE film 40 µm done; first good film seven minutes after the start.' },
{ id: 'e5', at: at(85), author: 'A. Brandt', category: 'safety', text: 'Guard door on the palletiser did not latch first time. Latched on the second try; maintenance told.', tag: 'plc.PAL.DOOR' },
{ id: 'e6', at: at(91), author: 'A. Brandt', category: 'maintenance', text: 'Maintenance coming within the hour for the palletiser door switch. Expect 20 minutes of stop.' }
];
let refuseNext = false;
const show = () => {
[log, viewer].forEach((one) => {
one.shift = { name: 'Shift A', from: shiftStart.toISOString(), to: shiftEnd.toISOString() };
one.entries = ENTRIES.map((entry) => Object.assign({}, entry));
});
};
show();
/* The station stores it - with its own clock and the name of whoever is signed in - and
hands the log back. The element wrote nothing. */
log.addEventListener('entryAdd', function (event) {
const entry = event.detail;
log.busy = true;
note.textContent = 'Storing the entry...';
setTimeout(function () {
log.busy = false;
if (refuseNext) {
refuseNext = false;
log.error = 'The station refused the entry: the session expired. Sign in again - the text is still in the box.';
note.textContent = 'Refused: the draft is back in the box.';
return;
}
ENTRIES.push({ id: 'e' + (ENTRIES.length + 1), at: new Date().toISOString(), author: 'A. Brandt', category: entry.category, text: entry.text, tag: entry.tag, correcting: entry.correcting });
show();
note.textContent = 'Stored: "' + entry.text.slice(0, 60) + (entry.text.length > 60 ? '…' : '') + '"' + (entry.correcting ? ', correcting an earlier entry.' : '.');
}, 600);
});
document.getElementById('refuse').addEventListener('click', function () {
refuseNext = true;
note.textContent = 'The next entry will be refused.';
});
});
return () => {
unmounted = true;
};
}, []);
return (
<div className="react-root" ref={root} theme="industrial">
<div className="smart-demo-container">
<section id="overview">
<h2>Smart.ShiftLog is the book on the control-room desk, kept where the screens are.</h2>
<p>The next shift needs to know what this one knows, and no tag says it: that the filler jammed twice and was cleared by hand, that maintenance is coming at ten, that the new film roll is thinner than the last. Kept on the station, the log cannot be lost with the notebook, it can be searched, and it names who wrote every line.</p>
</section>
<section>
<h2>A shift's log</h2>
<p>Handover entries carry the accent, safety entries the warning edge; everything else is text, as a log should be. <b>Ctrl+Enter</b> adds an entry; Enter alone starts a new line. Entries from before this shift sit under <i>Earlier shifts</i>.</p>
<div className="demo-row">
<ShiftLog id="log" label="Line 3 shift log" author="A. Brandt"></ShiftLog>
</div>
<div className="demo-actions">
<button type="button" id="refuse">Make the station refuse the next entry</button>
</div>
<p className="hint" id="note">The log raises <code>entryAdd</code>; this page plays the station that stores it.</p>
</section>
<section>
<h2>Corrections, not edits</h2>
<p>An entry is never changed. <b>Correct</b> starts a new entry that says which one it corrects, and the original stays, marked as corrected. A log that can be rewritten afterwards is a record of what somebody later wished had been known, not of what was known when - which is also what a regulated site needs.</p>
<p>If the station refuses an entry (it sets <code>error</code>), the draft comes back into the box rather than being lost.</p>
</section>
<section>
<h2>Read-only</h2>
<p><code>readOnly</code> removes the box and the Correct buttons: a supervisor's view of the same log.</p>
<div className="demo-row">
<ShiftLog id="viewer" label="Line 3 shift log (view only)" readOnly></ShiftLog>
</div>
</section>
</div>
</div>
);
};
export default App;
react/App.css
/* The panel is a real control 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-shift-log {
width: 720px;
max-width: 100%;
height: 620px;
}
/* ── 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.ShiftLog is the book on the control-room desk, kept where the screens are.</h2>
<p>The next shift needs to know what this one knows, and no tag says it: that the filler jammed twice
and was cleared by hand, that maintenance is coming at ten, that the new film roll is thinner than the
last. Kept on the station, the log cannot be lost with the notebook, it can be searched, and it names
who wrote every line.</p>
</section>
<section>
<h2>A shift's log</h2>
<p>Handover entries carry the accent, safety entries the warning edge; everything else is text, as a log
should be. <b>Ctrl+Enter</b> adds an entry; Enter alone starts a new line. Entries from before this shift
sit under <i>Earlier shifts</i>.</p>
<div class="demo-row">
<smart-shift-log id="log" label="Line 3 shift log" author="A. Brandt"></smart-shift-log>
</div>
<div class="demo-actions">
<button type="button" id="refuse">Make the station refuse the next entry</button>
</div>
<p class="hint" id="note">The log raises <code>entryAdd</code>; this page plays the station that stores it.</p>
</section>
<section>
<h2>Corrections, not edits</h2>
<p>An entry is never changed. <b>Correct</b> starts a new entry that says which one it corrects, and the
original stays, marked as corrected. A log that can be rewritten afterwards is a record of what somebody
later wished had been known, not of what was known when - which is also what a regulated site needs.</p>
<p>If the station refuses an entry (it sets <code>error</code>), the draft comes back into the box rather
than being lost.</p>
</section>
<section>
<h2>Read-only</h2>
<p><code>readOnly</code> removes the box and the Correct buttons: a supervisor's view of the same log.</p>
<div class="demo-row">
<smart-shift-log id="viewer" label="Line 3 shift log (view only)" read-only></smart-shift-log>
</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.shiftlog.js";
/* The shift log: the page plays the station, which stores each entry with its own clock and
the writer's name, and hands the log 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 log = document.getElementById('log'),
viewer = document.getElementById('viewer'),
note = document.getElementById('note'),
/* a shift two and a half hours in, whatever the time of day, so its entries are all behind us */
shiftStart = new Date(Date.now() - 150 * 60000),
shiftEnd = new Date(shiftStart.getTime() + 8 * 3600000),
at = (minutes) => new Date(shiftStart.getTime() + minutes * 60000).toISOString(),
ENTRIES = [
{ id: 'e1', at: at(-50), author: 'M. Keller', category: 'handover', text: 'Line running on product B. Filler 2 lost its bottle guide screw an hour before the end of the shift - temporary fix in place, part ordered. Keep an eye on it.', tag: 'plc.FILLER2' },
{ id: 'e2', at: at(-120), author: 'M. Keller', category: 'quality', text: 'Two cartons from lot 4471 rejected for underfill. Checkweigher recalibrated, OK since.' },
{ id: 'e3', at: at(12), author: 'A. Brandt', category: 'note', text: 'Took over from M. Keller. Filler 2 guide checked - holding.' },
{ id: 'e4', at: at(48), author: 'A. Brandt', category: 'production', text: 'Changeover to PE film 40 µm done; first good film seven minutes after the start.' },
{ id: 'e5', at: at(85), author: 'A. Brandt', category: 'safety', text: 'Guard door on the palletiser did not latch first time. Latched on the second try; maintenance told.', tag: 'plc.PAL.DOOR' },
{ id: 'e6', at: at(91), author: 'A. Brandt', category: 'maintenance', text: 'Maintenance coming within the hour for the palletiser door switch. Expect 20 minutes of stop.' }
];
let refuseNext = false;
const show = () => {
[log, viewer].forEach((one) => {
one.shift = { name: 'Shift A', from: shiftStart.toISOString(), to: shiftEnd.toISOString() };
one.entries = ENTRIES.map((entry) => Object.assign({}, entry));
});
};
show();
/* The station stores it - with its own clock and the name of whoever is signed in - and
hands the log back. The element wrote nothing. */
log.addEventListener('entryAdd', function (event) {
const entry = event.detail;
log.busy = true;
note.textContent = 'Storing the entry...';
setTimeout(function () {
log.busy = false;
if (refuseNext) {
refuseNext = false;
log.error = 'The station refused the entry: the session expired. Sign in again - the text is still in the box.';
note.textContent = 'Refused: the draft is back in the box.';
return;
}
ENTRIES.push({ id: 'e' + (ENTRIES.length + 1), at: new Date().toISOString(), author: 'A. Brandt', category: entry.category, text: entry.text, tag: entry.tag, correcting: entry.correcting });
show();
note.textContent = 'Stored: "' + entry.text.slice(0, 60) + (entry.text.length > 60 ? '…' : '') + '"' + (entry.correcting ? ', correcting an earlier entry.' : '.');
}, 600);
});
document.getElementById('refuse').addEventListener('click', function () {
refuseNext = true;
note.textContent = 'The next entry will be refused.';
});
});
});
}
};
</script>
<style>
/* The panel is a real control 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-shift-log {
width: 720px;
max-width: 100%;
height: 620px;
}
/* ── 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 shift's log
- Corrections, not edits
- Read-only