Component demo · Electronic records (21 CFR Part 11)
E-Signature
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>Electronic Signature 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.ESignature provides the electronic signature dialog described in 21 CFR Part 11: the record being signed, the meaning of the signature, the two signature components and the resulting manifestation.</h2>
<p>
The panel shows the record and its hash, asks for the meaning of the signature (§11.50), a user ID and a password (§11.200), and a reason when the procedure requires one. The component does not verify the password. It raises the <code>signRequest</code> event, the application checks the credentials against its user directory and calls <code>accept()</code> or <code>reject()</code>. After three rejections the panel is locked (§11.300(d)). The signature contains the hash of the signed record (§11.70), so a signature block under the record can report whether the record has changed.
</p>
</section>
<section>
<h2>Releasing a batch</h2>
<p>
The user directory in this demo contains <b>jdoe</b> (password <code>operator1</code>) and <b>asmith</b> (password <code>qa2026</code>). Only asmith is a QA approver. Releasing the batch requires an approval signature, after which the batch record is locked. Edit the yield after signing to see the signature block report that the record has changed. Enable <em>continuous session</em> to see a second signing by the same user ask for the password only.
</p>
<div class="release">
<div class="record">
<h3>Batch record <span id="batchId">B-2026-0912</span></h3>
<table class="record-fields">
<tr><th>Product</th><td>Ibuprofen 400 mg film-coated</td></tr>
<tr><th>Line</th><td>Line 3</td></tr>
<tr><th><label for="yield">Yield</label></th><td><input id="yield" type="number" value="98.4" step="0.1" /> %</td></tr>
<tr><th>Deviations</th><td id="deviations">0 open</td></tr>
</table>
<div class="record-actions">
<button id="approve" type="button" class="demo-button demo-button-primary">Sign for release</button>
<button id="review" type="button" class="demo-button">Sign as reviewed</button>
</div>
<div id="signatures" class="signatures">
<smart-signature-block id="releaseBlock"></smart-signature-block>
</div>
</div>
<div class="ceremony">
<smart-esignature id="sign" require-reason auto-close="0"></smart-esignature>
<div class="host">
<h3>Application</h3>
<ol id="log" class="log"></ol>
<div class="demo-actions">
<label><input type="checkbox" id="continuous" /> Continuous session (15 min)</label>
<label><input type="checkbox" id="touch" /> Touch density</label>
<button id="unlock" type="button" class="demo-button">Administrator: unlock</button>
</div>
</div>
</div>
</div>
<p class="hint">
Keyboard: Tab through the meaning, user ID, password, reason and Sign fields. Enter signs and Escape cancels. A rejection and the lock are announced to screen readers, and the manifestation is a status message.
</p>
</section>
</div>
<!-- scripts -->
<script type="module" src="node_modules/smart-industrial/source/modules/smart.esignature.js"></script>
<script type="module" src="node_modules/smart-industrial/source/modules/smart.signatureblock.js"></script>
<script type="module" src="index.js"></script>
<script>
/* Shown once every element has rendered, so nothing moves on the way in (styles/demos.css). */
addEventListener('load', function () {
var all = Array.prototype.slice.call(document.querySelectorAll('*')),
ready = function () { document.body.classList.add('ready'); };
Promise.all(all.map(function (e) { return e.localName.indexOf('smart-') === 0 ? customElements.whenDefined(e.localName) : null; })).then(function () {
return Promise.all(all.filter(function (e) { return typeof e.whenRendered === 'function' && !e.isRendered; }).map(function (e) { return e.whenRendered(); }));
}).then(function () { requestAnimationFrame(function () { requestAnimationFrame(ready); }); }, ready);
});
</script>
</body>
</html>
index.js
/// <reference path="../../../source/typescript/smart.elements.d.ts" />
/* A simulated directory and a batch record. The panel raises signRequest; the host asks the
directory - after a moment, as a server would - and answers accept() or reject(). The
signature the panel returns is what the record keeps. Replace `directory` with the call
to the identity provider; nothing else changes. */
window.onload = function () {
const sign = document.getElementById('sign'),
approve = document.getElementById('approve'),
review = document.getElementById('review'),
yieldInput = document.getElementById('yield'),
releaseBlock = document.getElementById('releaseBlock'),
signatures = document.getElementById('signatures'),
log = document.getElementById('log'),
continuous = document.getElementById('continuous'),
touch = document.getElementById('touch'),
unlock = document.getElementById('unlock');
const directory = {
users: {
jdoe: { password: 'operator1', name: 'John Doe', roles: ['operator'] },
asmith: { password: 'qa2026', name: 'Alice Smith', roles: ['operator', 'qa'] }
},
verify: function (userId, password) {
return new Promise(function (resolve) {
setTimeout(function () {
const user = directory.users[userId];
resolve(user && user.password === password ? { id: userId, name: user.name, roles: user.roles } : null);
}, 600);
});
}
};
let released = false;
function note(text) {
const item = document.createElement('li');
item.textContent = new Date().toLocaleTimeString('en-GB', { hour12: false }) + ' ' + text;
log.insertBefore(item, log.firstChild);
while (log.children.length > 8) {
log.removeChild(log.lastChild);
}
}
/* The record as it is now - what gets hashed into a signature and what the block
checks a signature against. */
function record() {
return {
id: 'B-2026-0912',
title: 'Batch record B-2026-0912',
fields: [
{ label: 'Product', value: 'Ibuprofen 400 mg film-coated' },
{ label: 'Line', value: 'Line 3' },
{ label: 'Yield', value: yieldInput.value + ' %' },
{ label: 'Deviations', value: '0 open' }
]
};
}
/* ── The host answers the panel ─────────────────────────────────────── */
sign.addEventListener('signRequest', function (event) {
const detail = event.detail;
note('signRequest ' + detail.userId + ' · ' + detail.meaning + (detail.continuous ? ' · password only' : ''));
directory.verify(detail.userId, detail.password).then(function (user) {
if (!user) {
note('directory: refused');
sign.reject();
return;
}
/* The authority check (§11.10(g)) is the host's: a valid password is not a
permission to approve. */
if (detail.meaning === 'approved' && user.roles.indexOf('qa') === -1) {
note('directory: ' + user.name + ' is not a QA approver');
sign.reject('not authorised to approve');
return;
}
note('directory: ' + user.name + ' verified');
sign.accept({ userName: user.name });
});
});
sign.addEventListener('lockout', function (event) {
note('lockout after ' + event.detail.attempts + ' attempts - the server has been told');
});
sign.addEventListener('cancel', function () {
note('cancelled');
});
/* ── The record asks for signatures ────────────────────────────────── */
approve.addEventListener('click', function () {
sign.sign(record(), { meaning: 'approved' }).then(function (signature) {
note('signed: ' + Smart.Industrial.Audit.manifest(signature));
releaseBlock.signature = signature;
releaseBlock.record = record();
released = true;
approve.disabled = true;
yieldInput.setAttribute('aria-describedby', 'releaseBlock');
remember(signature);
}, function (why) {
note('no signature (' + why.reason + ')');
});
});
review.addEventListener('click', function () {
sign.sign(record(), { meaning: 'reviewed' }).then(function (signature) {
const block = document.createElement('smart-signature-block');
block.compact = true;
block.signature = signature;
signatures.appendChild(block);
note('signed: ' + Smart.Industrial.Audit.manifest(signature));
remember(signature);
}, function () { });
});
/* Continuous session: the host tells the panel who is signed in, once a signing has
established who that is. */
function remember(signature) {
sign.sessionUserId = signature.userId;
sign.sessionUserName = signature.userName;
}
/* The record changes under its signature. */
yieldInput.addEventListener('input', function () {
if (released) {
releaseBlock.record = record();
note('yield edited after release - the block reports it');
}
});
/* ── Chrome ─────────────────────────────────────────────────────────── */
continuous.addEventListener('change', function () {
sign.continuousSession = continuous.checked;
if (!continuous.checked) {
sign.resetSession();
}
});
touch.addEventListener('change', function () {
sign.density = touch.checked ? 'touch' : 'normal';
});
unlock.addEventListener('click', function () {
sign.reset();
note('administrator reset the lock');
});
sign.lockMeaning = true;
/* The page opens on the signing itself: the release signature is requested as soon as
the record is shown. Escape or Cancel leaves the batch unsigned. */
setTimeout(function () {
approve.click();
}, 300);
};
styles.css
.release {
display: grid;
grid-template-columns: minmax(280px, 1fr) minmax(320px, 440px);
gap: 24px;
align-items: start;
}
@media (max-width: 900px) {
.release {
grid-template-columns: 1fr;
}
}
.record h3,
.host h3 {
margin: 0 0 8px;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.03em;
text-transform: uppercase;
}
.record-fields {
width: 100%;
border-collapse: collapse;
font-size: 13px;
margin-bottom: 12px;
}
.record-fields th,
.record-fields td {
padding: 6px 10px;
border-bottom: 1px solid var(--smart-border);
text-align: left;
}
.record-fields th {
width: 30%;
font-weight: 600;
opacity: 0.75;
}
.record-fields input {
width: 80px;
font: inherit;
padding: 2px 6px;
}
.record-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 12px;
}
.demo-button {
min-height: 34px;
padding: 0 14px;
font: inherit;
font-size: 13px;
font-weight: 600;
color: var(--smart-text-color);
background: var(--smart-surface);
border: 1px solid var(--smart-border);
border-radius: 4px;
cursor: pointer;
}
.demo-button-primary {
color: var(--smart-primary-color, #fff);
background: var(--smart-primary);
border-color: var(--smart-primary);
}
.demo-button[disabled] {
opacity: 0.5;
cursor: default;
}
.signatures {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.ceremony {
display: flex;
flex-direction: column;
gap: 16px;
}
.host {
padding: 10px 12px;
border: 1px solid var(--smart-border);
border-radius: 4px;
}
.log {
margin: 0;
padding: 0;
list-style: none;
font-size: 12px;
font-variant-numeric: tabular-nums;
opacity: 0.8;
min-height: 60px;
}
.log li {
padding: 2px 0;
}
.demo-actions {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
margin-top: 12px;
}
.demo-actions label {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 13px;
}
.hint {
margin-top: 10px;
font-size: 12px;
opacity: 0.75;
}
/* ── 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.ESignature provides the electronic signature dialog described in 21 CFR Part 11: the record being signed, the meaning of the signature, the two signature components and the resulting manifestation.</h2>
<p>
The panel shows the record and its hash, asks for the meaning of the signature (§11.50), a user ID and a password (§11.200), and a reason when the procedure requires one. The component does not verify the password. It raises the <code>signRequest</code> event, the application checks the credentials against its user directory and calls <code>accept()</code> or <code>reject()</code>. After three rejections the panel is locked (§11.300(d)). The signature contains the hash of the signed record (§11.70), so a signature block under the record can report whether the record has changed.
</p>
</section>
<section>
<h2>Releasing a batch</h2>
<p>
The user directory in this demo contains <b>jdoe</b> (password <code>operator1</code>) and <b>asmith</b> (password <code>qa2026</code>). Only asmith is a QA approver. Releasing the batch requires an approval signature, after which the batch record is locked. Edit the yield after signing to see the signature block report that the record has changed. Enable <em>continuous session</em> to see a second signing by the same user ask for the password only.
</p>
<div class="release">
<div class="record">
<h3>Batch record <span id="batchId">B-2026-0912</span></h3>
<table class="record-fields">
<tr><th>Product</th><td>Ibuprofen 400 mg film-coated</td></tr>
<tr><th>Line</th><td>Line 3</td></tr>
<tr><th><label for="yield">Yield</label></th><td><input id="yield" type="number" value="98.4" step="0.1" /> %</td></tr>
<tr><th>Deviations</th><td id="deviations">0 open</td></tr>
</table>
<div class="record-actions">
<button id="approve" type="button" class="demo-button demo-button-primary">Sign for release</button>
<button id="review" type="button" class="demo-button">Sign as reviewed</button>
</div>
<div id="signatures" class="signatures">
<smart-signature-block id="releaseBlock"></smart-signature-block>
</div>
</div>
<div class="ceremony">
<smart-esignature id="sign" [requireReason]="true" [autoClose]="0"></smart-esignature>
<div class="host">
<h3>Application</h3>
<ol id="log" class="log"></ol>
<div class="demo-actions">
<label><input type="checkbox" id="continuous" /> Continuous session (15 min)</label>
<label><input type="checkbox" id="touch" /> Touch density</label>
<button id="unlock" type="button" class="demo-button">Administrator: unlock</button>
</div>
</div>
</div>
</div>
<p class="hint">
Keyboard: Tab through the meaning, user ID, password, reason and Sign fields. Enter signs and Escape cancels. A rejection and the lock are announced to screen readers, and the manifestation is a status message.
</p>
</section>
</div>
</div>
angular/app.component.ts
import { Component, AfterViewInit, OnDestroy } from '@angular/core';
import { SignatureBlockModule } from 'smart-industrial/angular/signatureblock';
import { ESignatureModule } from 'smart-industrial/angular/esignature';
import { runDemo } from './demo';
/**
* Smart.UI Industrial - ESignature 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: [SignatureBlockModule, ESignatureModule],
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. */
declare const Smart: any;
/* A simulated directory and a batch record. The panel raises signRequest; the host asks the
directory - after a moment, as a server would - and answers accept() or reject(). The
signature the panel returns is what the record keeps. Replace `directory` with the call
to the identity provider; nothing else changes. */
export function runDemo(): void {
const sign = document.getElementById('sign'),
approve = document.getElementById('approve'),
review = document.getElementById('review'),
yieldInput = document.getElementById('yield'),
releaseBlock = document.getElementById('releaseBlock'),
signatures = document.getElementById('signatures'),
log = document.getElementById('log'),
continuous = document.getElementById('continuous'),
touch = document.getElementById('touch'),
unlock = document.getElementById('unlock');
const directory = {
users: {
jdoe: { password: 'operator1', name: 'John Doe', roles: ['operator'] },
asmith: { password: 'qa2026', name: 'Alice Smith', roles: ['operator', 'qa'] }
},
verify: function (userId, password) {
return new Promise(function (resolve) {
setTimeout(function () {
const user = directory.users[userId];
resolve(user && user.password === password ? { id: userId, name: user.name, roles: user.roles } : null);
}, 600);
});
}
};
let released = false;
function note(text) {
const item = document.createElement('li');
item.textContent = new Date().toLocaleTimeString('en-GB', { hour12: false }) + ' ' + text;
log.insertBefore(item, log.firstChild);
while (log.children.length > 8) {
log.removeChild(log.lastChild);
}
}
/* The record as it is now - what gets hashed into a signature and what the block
checks a signature against. */
function record() {
return {
id: 'B-2026-0912',
title: 'Batch record B-2026-0912',
fields: [
{ label: 'Product', value: 'Ibuprofen 400 mg film-coated' },
{ label: 'Line', value: 'Line 3' },
{ label: 'Yield', value: yieldInput.value + ' %' },
{ label: 'Deviations', value: '0 open' }
]
};
}
/* ── The host answers the panel ─────────────────────────────────────── */
sign.addEventListener('signRequest', function (event) {
const detail = event.detail;
note('signRequest ' + detail.userId + ' · ' + detail.meaning + (detail.continuous ? ' · password only' : ''));
directory.verify(detail.userId, detail.password).then(function (user) {
if (!user) {
note('directory: refused');
sign.reject();
return;
}
/* The authority check (§11.10(g)) is the host's: a valid password is not a
permission to approve. */
if (detail.meaning === 'approved' && user.roles.indexOf('qa') === -1) {
note('directory: ' + user.name + ' is not a QA approver');
sign.reject('not authorised to approve');
return;
}
note('directory: ' + user.name + ' verified');
sign.accept({ userName: user.name });
});
});
sign.addEventListener('lockout', function (event) {
note('lockout after ' + event.detail.attempts + ' attempts - the server has been told');
});
sign.addEventListener('cancel', function () {
note('cancelled');
});
/* ── The record asks for signatures ────────────────────────────────── */
approve.addEventListener('click', function () {
sign.sign(record(), { meaning: 'approved' }).then(function (signature) {
note('signed: ' + Smart.Industrial.Audit.manifest(signature));
releaseBlock.signature = signature;
releaseBlock.record = record();
released = true;
approve.disabled = true;
yieldInput.setAttribute('aria-describedby', 'releaseBlock');
remember(signature);
}, function (why) {
note('no signature (' + why.reason + ')');
});
});
review.addEventListener('click', function () {
sign.sign(record(), { meaning: 'reviewed' }).then(function (signature) {
const block = document.createElement('smart-signature-block');
block.compact = true;
block.signature = signature;
signatures.appendChild(block);
note('signed: ' + Smart.Industrial.Audit.manifest(signature));
remember(signature);
}, function () { });
});
/* Continuous session: the host tells the panel who is signed in, once a signing has
established who that is. */
function remember(signature) {
sign.sessionUserId = signature.userId;
sign.sessionUserName = signature.userName;
}
/* The record changes under its signature. */
yieldInput.addEventListener('input', function () {
if (released) {
releaseBlock.record = record();
note('yield edited after release - the block reports it');
}
});
/* ── Chrome ─────────────────────────────────────────────────────────── */
continuous.addEventListener('change', function () {
sign.continuousSession = continuous.checked;
if (!continuous.checked) {
sign.resetSession();
}
});
touch.addEventListener('change', function () {
sign.density = touch.checked ? 'touch' : 'normal';
});
unlock.addEventListener('click', function () {
sign.reset();
note('administrator reset the lock');
});
sign.lockMeaning = true;
/* The page opens on the signing itself: the release signature is requested as soon as
the record is shown. Escape or Cancel leaves the batch unsigned. */
setTimeout(function () {
approve.click();
}, 300);
}
angular/app.component.css
.release {
display: grid;
grid-template-columns: minmax(280px, 1fr) minmax(320px, 440px);
gap: 24px;
align-items: start;
}
@media (max-width: 900px) {
.release {
grid-template-columns: 1fr;
}
}
.record h3,
.host h3 {
margin: 0 0 8px;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.03em;
text-transform: uppercase;
}
.record-fields {
width: 100%;
border-collapse: collapse;
font-size: 13px;
margin-bottom: 12px;
}
.record-fields th,
.record-fields td {
padding: 6px 10px;
border-bottom: 1px solid var(--smart-border);
text-align: left;
}
.record-fields th {
width: 30%;
font-weight: 600;
opacity: 0.75;
}
.record-fields input {
width: 80px;
font: inherit;
padding: 2px 6px;
}
.record-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 12px;
}
.demo-button {
min-height: 34px;
padding: 0 14px;
font: inherit;
font-size: 13px;
font-weight: 600;
color: var(--smart-text-color);
background: var(--smart-surface);
border: 1px solid var(--smart-border);
border-radius: 4px;
cursor: pointer;
}
.demo-button-primary {
color: var(--smart-primary-color, #fff);
background: var(--smart-primary);
border-color: var(--smart-primary);
}
.demo-button[disabled] {
opacity: 0.5;
cursor: default;
}
.signatures {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.ceremony {
display: flex;
flex-direction: column;
gap: 16px;
}
.host {
padding: 10px 12px;
border: 1px solid var(--smart-border);
border-radius: 4px;
}
.log {
margin: 0;
padding: 0;
list-style: none;
font-size: 12px;
font-variant-numeric: tabular-nums;
opacity: 0.8;
min-height: 60px;
}
.log li {
padding: 2px 0;
}
.demo-actions {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
margin-top: 12px;
}
.demo-actions label {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 13px;
}
.hint {
margin-top: 10px;
font-size: 12px;
opacity: 0.75;
}
/* ── 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 { SignatureBlock } from 'smart-industrial/react/signatureblock';
import { ESignature } from 'smart-industrial/react/esignature';
const Smart = window.Smart;
/* The demo logic is the HTML demo's: it feeds the elements the way a plant's data would. */
/* A simulated directory and a batch record. The panel raises signRequest; the host asks the
directory - after a moment, as a server would - and answers accept() or reject(). The
signature the panel returns is what the record keeps. Replace `directory` with the call
to the identity provider; nothing else changes. */
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 sign = document.getElementById('sign'),
approve = document.getElementById('approve'),
review = document.getElementById('review'),
yieldInput = document.getElementById('yield'),
releaseBlock = document.getElementById('releaseBlock'),
signatures = document.getElementById('signatures'),
log = document.getElementById('log'),
continuous = document.getElementById('continuous'),
touch = document.getElementById('touch'),
unlock = document.getElementById('unlock');
const directory = {
users: {
jdoe: { password: 'operator1', name: 'John Doe', roles: ['operator'] },
asmith: { password: 'qa2026', name: 'Alice Smith', roles: ['operator', 'qa'] }
},
verify: function (userId, password) {
return new Promise(function (resolve) {
setTimeout(function () {
const user = directory.users[userId];
resolve(user && user.password === password ? { id: userId, name: user.name, roles: user.roles } : null);
}, 600);
});
}
};
let released = false;
function note(text) {
const item = document.createElement('li');
item.textContent = new Date().toLocaleTimeString('en-GB', { hour12: false }) + ' ' + text;
log.insertBefore(item, log.firstChild);
while (log.children.length > 8) {
log.removeChild(log.lastChild);
}
}
/* The record as it is now - what gets hashed into a signature and what the block
checks a signature against. */
function record() {
return {
id: 'B-2026-0912',
title: 'Batch record B-2026-0912',
fields: [
{ label: 'Product', value: 'Ibuprofen 400 mg film-coated' },
{ label: 'Line', value: 'Line 3' },
{ label: 'Yield', value: yieldInput.value + ' %' },
{ label: 'Deviations', value: '0 open' }
]
};
}
/* ── The host answers the panel ─────────────────────────────────────── */
sign.addEventListener('signRequest', function (event) {
const detail = event.detail;
note('signRequest ' + detail.userId + ' · ' + detail.meaning + (detail.continuous ? ' · password only' : ''));
directory.verify(detail.userId, detail.password).then(function (user) {
if (!user) {
note('directory: refused');
sign.reject();
return;
}
/* The authority check (§11.10(g)) is the host's: a valid password is not a
permission to approve. */
if (detail.meaning === 'approved' && user.roles.indexOf('qa') === -1) {
note('directory: ' + user.name + ' is not a QA approver');
sign.reject('not authorised to approve');
return;
}
note('directory: ' + user.name + ' verified');
sign.accept({ userName: user.name });
});
});
sign.addEventListener('lockout', function (event) {
note('lockout after ' + event.detail.attempts + ' attempts - the server has been told');
});
sign.addEventListener('cancel', function () {
note('cancelled');
});
/* ── The record asks for signatures ────────────────────────────────── */
approve.addEventListener('click', function () {
sign.sign(record(), { meaning: 'approved' }).then(function (signature) {
note('signed: ' + Smart.Industrial.Audit.manifest(signature));
releaseBlock.signature = signature;
releaseBlock.record = record();
released = true;
approve.disabled = true;
yieldInput.setAttribute('aria-describedby', 'releaseBlock');
remember(signature);
}, function (why) {
note('no signature (' + why.reason + ')');
});
});
review.addEventListener('click', function () {
sign.sign(record(), { meaning: 'reviewed' }).then(function (signature) {
const block = document.createElement('smart-signature-block');
block.compact = true;
block.signature = signature;
signatures.appendChild(block);
note('signed: ' + Smart.Industrial.Audit.manifest(signature));
remember(signature);
}, function () { });
});
/* Continuous session: the host tells the panel who is signed in, once a signing has
established who that is. */
function remember(signature) {
sign.sessionUserId = signature.userId;
sign.sessionUserName = signature.userName;
}
/* The record changes under its signature. */
yieldInput.addEventListener('input', function () {
if (released) {
releaseBlock.record = record();
note('yield edited after release - the block reports it');
}
});
/* ── Chrome ─────────────────────────────────────────────────────────── */
continuous.addEventListener('change', function () {
sign.continuousSession = continuous.checked;
if (!continuous.checked) {
sign.resetSession();
}
});
touch.addEventListener('change', function () {
sign.density = touch.checked ? 'touch' : 'normal';
});
unlock.addEventListener('click', function () {
sign.reset();
note('administrator reset the lock');
});
sign.lockMeaning = true;
/* The page opens on the signing itself: the release signature is requested as soon as
the record is shown. Escape or Cancel leaves the batch unsigned. */
setTimeout(function () {
approve.click();
}, 300);
});
return () => {
unmounted = true;
};
}, []);
return (
<div className="react-root" ref={root} theme="industrial">
<div className="smart-demo-container">
<section id="overview">
<h2>Smart.ESignature provides the electronic signature dialog described in 21 CFR Part 11: the record being signed, the meaning of the signature, the two signature components and the resulting manifestation.</h2>
<p>The panel shows the record and its hash, asks for the meaning of the signature (§11.50), a user ID and a password (§11.200), and a reason when the procedure requires one. The component does not verify the password. It raises the <code>signRequest</code> event, the application checks the credentials against its user directory and calls <code>accept()</code> or <code>reject()</code>. After three rejections the panel is locked (§11.300(d)). The signature contains the hash of the signed record (§11.70), so a signature block under the record can report whether the record has changed.</p>
</section>
<section>
<h2>Releasing a batch</h2>
<p>The user directory in this demo contains <b>jdoe</b> (password <code>operator1</code>) and <b>asmith</b> (password <code>qa2026</code>). Only asmith is a QA approver. Releasing the batch requires an approval signature, after which the batch record is locked. Edit the yield after signing to see the signature block report that the record has changed. Enable <em>continuous session</em> to see a second signing by the same user ask for the password only.</p>
<div className="release">
<div className="record">
<h3>Batch record <span id="batchId">B-2026-0912</span></h3>
<table className="record-fields">
<tbody>
<tr>
<th>Product</th>
<td>Ibuprofen 400 mg film-coated</td>
</tr>
<tr>
<th>Line</th>
<td>Line 3</td>
</tr>
<tr>
<th>
<label htmlFor="yield">Yield</label>
</th>
<td><input id="yield" type="number" defaultValue="98.4" step="0.1" /> %</td>
</tr>
<tr>
<th>Deviations</th>
<td id="deviations">0 open</td>
</tr>
</tbody>
</table>
<div className="record-actions">
<button id="approve" type="button" className="demo-button demo-button-primary">Sign for release</button>
<button id="review" type="button" className="demo-button">Sign as reviewed</button>
</div>
<div id="signatures" className="signatures">
<SignatureBlock id="releaseBlock"></SignatureBlock>
</div>
</div>
<div className="ceremony">
<ESignature id="sign" requireReason autoClose={0}></ESignature>
<div className="host">
<h3>Application</h3>
<ol id="log" className="log"></ol>
<div className="demo-actions">
<label><input type="checkbox" id="continuous" /> Continuous session (15 min)</label>
<label><input type="checkbox" id="touch" /> Touch density</label>
<button id="unlock" type="button" className="demo-button">Administrator: unlock</button>
</div>
</div>
</div>
</div>
<p className="hint">Keyboard: Tab through the meaning, user ID, password, reason and Sign fields. Enter signs and Escape cancels. A rejection and the lock are announced to screen readers, and the manifestation is a status message.</p>
</section>
</div>
</div>
);
};
export default App;
react/App.css
.release {
display: grid;
grid-template-columns: minmax(280px, 1fr) minmax(320px, 440px);
gap: 24px;
align-items: start;
}
@media (max-width: 900px) {
.release {
grid-template-columns: 1fr;
}
}
.record h3,
.host h3 {
margin: 0 0 8px;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.03em;
text-transform: uppercase;
}
.record-fields {
width: 100%;
border-collapse: collapse;
font-size: 13px;
margin-bottom: 12px;
}
.record-fields th,
.record-fields td {
padding: 6px 10px;
border-bottom: 1px solid var(--smart-border);
text-align: left;
}
.record-fields th {
width: 30%;
font-weight: 600;
opacity: 0.75;
}
.record-fields input {
width: 80px;
font: inherit;
padding: 2px 6px;
}
.record-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 12px;
}
.demo-button {
min-height: 34px;
padding: 0 14px;
font: inherit;
font-size: 13px;
font-weight: 600;
color: var(--smart-text-color);
background: var(--smart-surface);
border: 1px solid var(--smart-border);
border-radius: 4px;
cursor: pointer;
}
.demo-button-primary {
color: var(--smart-primary-color, #fff);
background: var(--smart-primary);
border-color: var(--smart-primary);
}
.demo-button[disabled] {
opacity: 0.5;
cursor: default;
}
.signatures {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.ceremony {
display: flex;
flex-direction: column;
gap: 16px;
}
.host {
padding: 10px 12px;
border: 1px solid var(--smart-border);
border-radius: 4px;
}
.log {
margin: 0;
padding: 0;
list-style: none;
font-size: 12px;
font-variant-numeric: tabular-nums;
opacity: 0.8;
min-height: 60px;
}
.log li {
padding: 2px 0;
}
.demo-actions {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
margin-top: 12px;
}
.demo-actions label {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 13px;
}
.hint {
margin-top: 10px;
font-size: 12px;
opacity: 0.75;
}
/* ── 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.ESignature provides the electronic signature dialog described in 21 CFR Part 11: the record being signed, the meaning of the signature, the two signature components and the resulting manifestation.</h2>
<p>
The panel shows the record and its hash, asks for the meaning of the signature (§11.50), a user ID and a password (§11.200), and a reason when the procedure requires one. The component does not verify the password. It raises the <code>signRequest</code> event, the application checks the credentials against its user directory and calls <code>accept()</code> or <code>reject()</code>. After three rejections the panel is locked (§11.300(d)). The signature contains the hash of the signed record (§11.70), so a signature block under the record can report whether the record has changed.
</p>
</section>
<section>
<h2>Releasing a batch</h2>
<p>
The user directory in this demo contains <b>jdoe</b> (password <code>operator1</code>) and <b>asmith</b> (password <code>qa2026</code>). Only asmith is a QA approver. Releasing the batch requires an approval signature, after which the batch record is locked. Edit the yield after signing to see the signature block report that the record has changed. Enable <em>continuous session</em> to see a second signing by the same user ask for the password only.
</p>
<div class="release">
<div class="record">
<h3>Batch record <span id="batchId">B-2026-0912</span></h3>
<table class="record-fields">
<tr><th>Product</th><td>Ibuprofen 400 mg film-coated</td></tr>
<tr><th>Line</th><td>Line 3</td></tr>
<tr><th><label for="yield">Yield</label></th><td><input id="yield" type="number" value="98.4" step="0.1" /> %</td></tr>
<tr><th>Deviations</th><td id="deviations">0 open</td></tr>
</table>
<div class="record-actions">
<button id="approve" type="button" class="demo-button demo-button-primary">Sign for release</button>
<button id="review" type="button" class="demo-button">Sign as reviewed</button>
</div>
<div id="signatures" class="signatures">
<smart-signature-block id="releaseBlock"></smart-signature-block>
</div>
</div>
<div class="ceremony">
<smart-esignature id="sign" require-reason auto-close="0"></smart-esignature>
<div class="host">
<h3>Application</h3>
<ol id="log" class="log"></ol>
<div class="demo-actions">
<label><input type="checkbox" id="continuous" /> Continuous session (15 min)</label>
<label><input type="checkbox" id="touch" /> Touch density</label>
<button id="unlock" type="button" class="demo-button">Administrator: unlock</button>
</div>
</div>
</div>
</div>
<p class="hint">
Keyboard: Tab through the meaning, user ID, password, reason and Sign fields. Enter signs and Escape cancels. A rejection and the lock are announced to screen readers, and the manifestation is a status message.
</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.esignature.js";
import "smart-industrial/source/modules/smart.signatureblock.js";
const Smart = window.Smart;
/* A simulated directory and a batch record. The panel raises signRequest; the host asks the
directory - after a moment, as a server would - and answers accept() or reject(). The
signature the panel returns is what the record keeps. Replace `directory` with the call
to the identity provider; nothing else changes. */
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 sign = document.getElementById('sign'),
approve = document.getElementById('approve'),
review = document.getElementById('review'),
yieldInput = document.getElementById('yield'),
releaseBlock = document.getElementById('releaseBlock'),
signatures = document.getElementById('signatures'),
log = document.getElementById('log'),
continuous = document.getElementById('continuous'),
touch = document.getElementById('touch'),
unlock = document.getElementById('unlock');
const directory = {
users: {
jdoe: { password: 'operator1', name: 'John Doe', roles: ['operator'] },
asmith: { password: 'qa2026', name: 'Alice Smith', roles: ['operator', 'qa'] }
},
verify: function (userId, password) {
return new Promise(function (resolve) {
setTimeout(function () {
const user = directory.users[userId];
resolve(user && user.password === password ? { id: userId, name: user.name, roles: user.roles } : null);
}, 600);
});
}
};
let released = false;
function note(text) {
const item = document.createElement('li');
item.textContent = new Date().toLocaleTimeString('en-GB', { hour12: false }) + ' ' + text;
log.insertBefore(item, log.firstChild);
while (log.children.length > 8) {
log.removeChild(log.lastChild);
}
}
/* The record as it is now - what gets hashed into a signature and what the block
checks a signature against. */
function record() {
return {
id: 'B-2026-0912',
title: 'Batch record B-2026-0912',
fields: [
{ label: 'Product', value: 'Ibuprofen 400 mg film-coated' },
{ label: 'Line', value: 'Line 3' },
{ label: 'Yield', value: yieldInput.value + ' %' },
{ label: 'Deviations', value: '0 open' }
]
};
}
/* ── The host answers the panel ─────────────────────────────────────── */
sign.addEventListener('signRequest', function (event) {
const detail = event.detail;
note('signRequest ' + detail.userId + ' · ' + detail.meaning + (detail.continuous ? ' · password only' : ''));
directory.verify(detail.userId, detail.password).then(function (user) {
if (!user) {
note('directory: refused');
sign.reject();
return;
}
/* The authority check (§11.10(g)) is the host's: a valid password is not a
permission to approve. */
if (detail.meaning === 'approved' && user.roles.indexOf('qa') === -1) {
note('directory: ' + user.name + ' is not a QA approver');
sign.reject('not authorised to approve');
return;
}
note('directory: ' + user.name + ' verified');
sign.accept({ userName: user.name });
});
});
sign.addEventListener('lockout', function (event) {
note('lockout after ' + event.detail.attempts + ' attempts - the server has been told');
});
sign.addEventListener('cancel', function () {
note('cancelled');
});
/* ── The record asks for signatures ────────────────────────────────── */
approve.addEventListener('click', function () {
sign.sign(record(), { meaning: 'approved' }).then(function (signature) {
note('signed: ' + Smart.Industrial.Audit.manifest(signature));
releaseBlock.signature = signature;
releaseBlock.record = record();
released = true;
approve.disabled = true;
yieldInput.setAttribute('aria-describedby', 'releaseBlock');
remember(signature);
}, function (why) {
note('no signature (' + why.reason + ')');
});
});
review.addEventListener('click', function () {
sign.sign(record(), { meaning: 'reviewed' }).then(function (signature) {
const block = document.createElement('smart-signature-block');
block.compact = true;
block.signature = signature;
signatures.appendChild(block);
note('signed: ' + Smart.Industrial.Audit.manifest(signature));
remember(signature);
}, function () { });
});
/* Continuous session: the host tells the panel who is signed in, once a signing has
established who that is. */
function remember(signature) {
sign.sessionUserId = signature.userId;
sign.sessionUserName = signature.userName;
}
/* The record changes under its signature. */
yieldInput.addEventListener('input', function () {
if (released) {
releaseBlock.record = record();
note('yield edited after release - the block reports it');
}
});
/* ── Chrome ─────────────────────────────────────────────────────────── */
continuous.addEventListener('change', function () {
sign.continuousSession = continuous.checked;
if (!continuous.checked) {
sign.resetSession();
}
});
touch.addEventListener('change', function () {
sign.density = touch.checked ? 'touch' : 'normal';
});
unlock.addEventListener('click', function () {
sign.reset();
note('administrator reset the lock');
});
sign.lockMeaning = true;
/* The page opens on the signing itself: the release signature is requested as soon as
the record is shown. Escape or Cancel leaves the batch unsigned. */
setTimeout(function () {
approve.click();
}, 300);
});
});
}
};
</script>
<style>
.release {
display: grid;
grid-template-columns: minmax(280px, 1fr) minmax(320px, 440px);
gap: 24px;
align-items: start;
}
@media (max-width: 900px) {
.release {
grid-template-columns: 1fr;
}
}
.record h3,
.host h3 {
margin: 0 0 8px;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.03em;
text-transform: uppercase;
}
.record-fields {
width: 100%;
border-collapse: collapse;
font-size: 13px;
margin-bottom: 12px;
}
.record-fields th,
.record-fields td {
padding: 6px 10px;
border-bottom: 1px solid var(--smart-border);
text-align: left;
}
.record-fields th {
width: 30%;
font-weight: 600;
opacity: 0.75;
}
.record-fields input {
width: 80px;
font: inherit;
padding: 2px 6px;
}
.record-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 12px;
}
.demo-button {
min-height: 34px;
padding: 0 14px;
font: inherit;
font-size: 13px;
font-weight: 600;
color: var(--smart-text-color);
background: var(--smart-surface);
border: 1px solid var(--smart-border);
border-radius: 4px;
cursor: pointer;
}
.demo-button-primary {
color: var(--smart-primary-color, #fff);
background: var(--smart-primary);
border-color: var(--smart-primary);
}
.demo-button[disabled] {
opacity: 0.5;
cursor: default;
}
.signatures {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.ceremony {
display: flex;
flex-direction: column;
gap: 16px;
}
.host {
padding: 10px 12px;
border: 1px solid var(--smart-border);
border-radius: 4px;
}
.log {
margin: 0;
padding: 0;
list-style: none;
font-size: 12px;
font-variant-numeric: tabular-nums;
opacity: 0.8;
min-height: 60px;
}
.log li {
padding: 2px 0;
}
.demo-actions {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
margin-top: 12px;
}
.demo-actions label {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 13px;
}
.hint {
margin-top: 10px;
font-size: 12px;
opacity: 0.75;
}
/* ── 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
- Releasing a batch
- Batch record B-2026-0912
- Application