Component demo · Electronic records (21 CFR Part 11)
Signature Block
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>Signature Block 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.SignatureBlock displays the signature manifestation of a signed record: the meaning of the signature, the printed name of the signer, the date and time, and whether the record is unchanged since it was signed.</h2>
<p>
21 CFR Part 11 §11.50 requires a signed electronic record to show the printed name of the signer, the date and time of signing and the meaning of the signature. §11.70 requires the signature to be linked to its record so it cannot be transferred to another record. The component displays the manifestation and, when the current record is provided, compares it with the hash stored in the signature. The signature object is the one produced by <code>smart-esignature</code>.
</p>
</section>
<section>
<h2>A test result with three signatures</h2>
<div class="result">
<table class="result-fields">
<tr><th>Test</th><td>Dissolution, 30 min</td></tr>
<tr><th>Batch</th><td>B-2026-0912</td></tr>
<tr><th>Result</th><td><span id="resultValue">87.2</span> % (limit ≥ 80 %)</td></tr>
<tr><th>Verdict</th><td>Pass</td></tr>
</table>
<div class="blocks">
<smart-signature-block id="performed"></smart-signature-block>
<smart-signature-block id="reviewed"></smart-signature-block>
<smart-signature-block id="approved"></smart-signature-block>
</div>
</div>
<div class="demo-actions">
<label><input type="checkbox" id="alter" /> Alter the result after signing</label>
<label><input type="checkbox" id="compact" /> Compact</label>
<label><input type="checkbox" id="hash" /> Show the record hash</label>
<label><input type="checkbox" id="noId" /> Hide user IDs</label>
</div>
<p class="hint">
Each block is an accessible group named by its content, for example "Signature: Approved by Alice Smith, 2026-09-18 14:32:00 +03:00, Batch release, Record matches signature", so the verification result is available as text and not only as a colour.
</p>
</section>
</div>
<!-- scripts -->
<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" />
/* Three signatures on one test result, as smart-esignature would have produced them - each
carrying the hash of the record as it was when signed. The blocks are given the record
as it is now, so altering the result turns every verdict to "altered". */
window.onload = function () {
const Audit = Smart.Industrial.Audit,
performed = document.getElementById('performed'),
reviewed = document.getElementById('reviewed'),
approved = document.getElementById('approved'),
resultValue = document.getElementById('resultValue'),
alter = document.getElementById('alter'),
compact = document.getElementById('compact'),
hash = document.getElementById('hash'),
noId = document.getElementById('noId'),
blocks = [performed, reviewed, approved];
function record() {
return { test: 'Dissolution, 30 min', batch: 'B-2026-0912', result: Number(resultValue.textContent), verdict: 'Pass' };
}
/* Signed when the result read 87.2. */
const signedHash = Audit.recordHash(record());
performed.signature = { id: 's1', userId: 'jdoe', userName: 'John Doe', meaning: 'authored', at: '2026-09-18T11:05:12.000+03:00', recordHash: signedHash };
reviewed.signature = { id: 's2', userId: 'rkim', userName: 'Rachel Kim', meaning: 'reviewed', at: '2026-09-18T13:48:30.000+03:00', reason: 'Second-person review', recordHash: signedHash };
approved.signature = { id: 's3', userId: 'asmith', userName: 'Alice Smith', meaning: 'approved', at: '2026-09-18T14:32:00.000+03:00', reason: 'Batch release', recordHash: signedHash };
function verify() {
blocks.forEach(function (block) {
block.record = record();
});
}
alter.addEventListener('change', function () {
resultValue.textContent = alter.checked ? '79.2' : '87.2';
verify();
});
compact.addEventListener('change', function () {
blocks.forEach(function (block) {
block.compact = compact.checked;
});
});
hash.addEventListener('change', function () {
blocks.forEach(function (block) {
block.showHash = hash.checked;
});
});
noId.addEventListener('change', function () {
blocks.forEach(function (block) {
block.showUserId = !noId.checked;
});
});
verify();
};
styles.css
.result {
display: grid;
grid-template-columns: minmax(260px, 420px) minmax(280px, 1fr);
gap: 24px;
align-items: start;
}
@media (max-width: 900px) {
.result {
grid-template-columns: 1fr;
}
}
.result-fields {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.result-fields th,
.result-fields td {
padding: 6px 10px;
border-bottom: 1px solid var(--smart-border);
text-align: left;
}
.result-fields th {
width: 30%;
font-weight: 600;
opacity: 0.75;
}
.blocks {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.demo-actions {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 14px;
margin-top: 16px;
}
.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.SignatureBlock displays the signature manifestation of a signed record: the meaning of the signature, the printed name of the signer, the date and time, and whether the record is unchanged since it was signed.</h2>
<p>
21 CFR Part 11 §11.50 requires a signed electronic record to show the printed name of the signer, the date and time of signing and the meaning of the signature. §11.70 requires the signature to be linked to its record so it cannot be transferred to another record. The component displays the manifestation and, when the current record is provided, compares it with the hash stored in the signature. The signature object is the one produced by <code>smart-esignature</code>.
</p>
</section>
<section>
<h2>A test result with three signatures</h2>
<div class="result">
<table class="result-fields">
<tr><th>Test</th><td>Dissolution, 30 min</td></tr>
<tr><th>Batch</th><td>B-2026-0912</td></tr>
<tr><th>Result</th><td><span id="resultValue">87.2</span> % (limit ≥ 80 %)</td></tr>
<tr><th>Verdict</th><td>Pass</td></tr>
</table>
<div class="blocks">
<smart-signature-block id="performed"></smart-signature-block>
<smart-signature-block id="reviewed"></smart-signature-block>
<smart-signature-block id="approved"></smart-signature-block>
</div>
</div>
<div class="demo-actions">
<label><input type="checkbox" id="alter" /> Alter the result after signing</label>
<label><input type="checkbox" id="compact" /> Compact</label>
<label><input type="checkbox" id="hash" /> Show the record hash</label>
<label><input type="checkbox" id="noId" /> Hide user IDs</label>
</div>
<p class="hint">
Each block is an accessible group named by its content, for example "Signature: Approved by Alice Smith, 2026-09-18 14:32:00 +03:00, Batch release, Record matches signature", so the verification result is available as text and not only as a colour.
</p>
</section>
</div>
</div>
angular/app.component.ts
import { Component, AfterViewInit, OnDestroy } from '@angular/core';
import { SignatureBlockModule } from 'smart-industrial/angular/signatureblock';
import { runDemo } from './demo';
/**
* Smart.UI Industrial - SignatureBlock 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],
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;
/* Three signatures on one test result, as smart-esignature would have produced them - each
carrying the hash of the record as it was when signed. The blocks are given the record
as it is now, so altering the result turns every verdict to "altered". */
export function runDemo(): void {
const Audit = Smart.Industrial.Audit,
performed = document.getElementById('performed'),
reviewed = document.getElementById('reviewed'),
approved = document.getElementById('approved'),
resultValue = document.getElementById('resultValue'),
alter = document.getElementById('alter'),
compact = document.getElementById('compact'),
hash = document.getElementById('hash'),
noId = document.getElementById('noId'),
blocks = [performed, reviewed, approved];
function record() {
return { test: 'Dissolution, 30 min', batch: 'B-2026-0912', result: Number(resultValue.textContent), verdict: 'Pass' };
}
/* Signed when the result read 87.2. */
const signedHash = Audit.recordHash(record());
performed.signature = { id: 's1', userId: 'jdoe', userName: 'John Doe', meaning: 'authored', at: '2026-09-18T11:05:12.000+03:00', recordHash: signedHash };
reviewed.signature = { id: 's2', userId: 'rkim', userName: 'Rachel Kim', meaning: 'reviewed', at: '2026-09-18T13:48:30.000+03:00', reason: 'Second-person review', recordHash: signedHash };
approved.signature = { id: 's3', userId: 'asmith', userName: 'Alice Smith', meaning: 'approved', at: '2026-09-18T14:32:00.000+03:00', reason: 'Batch release', recordHash: signedHash };
function verify() {
blocks.forEach(function (block) {
block.record = record();
});
}
alter.addEventListener('change', function () {
resultValue.textContent = alter.checked ? '79.2' : '87.2';
verify();
});
compact.addEventListener('change', function () {
blocks.forEach(function (block) {
block.compact = compact.checked;
});
});
hash.addEventListener('change', function () {
blocks.forEach(function (block) {
block.showHash = hash.checked;
});
});
noId.addEventListener('change', function () {
blocks.forEach(function (block) {
block.showUserId = !noId.checked;
});
});
verify();
}
angular/app.component.css
.result {
display: grid;
grid-template-columns: minmax(260px, 420px) minmax(280px, 1fr);
gap: 24px;
align-items: start;
}
@media (max-width: 900px) {
.result {
grid-template-columns: 1fr;
}
}
.result-fields {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.result-fields th,
.result-fields td {
padding: 6px 10px;
border-bottom: 1px solid var(--smart-border);
text-align: left;
}
.result-fields th {
width: 30%;
font-weight: 600;
opacity: 0.75;
}
.blocks {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.demo-actions {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 14px;
margin-top: 16px;
}
.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';
const Smart = window.Smart;
/* The demo logic is the HTML demo's: it feeds the elements the way a plant's data would. */
/* Three signatures on one test result, as smart-esignature would have produced them - each
carrying the hash of the record as it was when signed. The blocks are given the record
as it is now, so altering the result turns every verdict to "altered". */
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 Audit = Smart.Industrial.Audit,
performed = document.getElementById('performed'),
reviewed = document.getElementById('reviewed'),
approved = document.getElementById('approved'),
resultValue = document.getElementById('resultValue'),
alter = document.getElementById('alter'),
compact = document.getElementById('compact'),
hash = document.getElementById('hash'),
noId = document.getElementById('noId'),
blocks = [performed, reviewed, approved];
function record() {
return { test: 'Dissolution, 30 min', batch: 'B-2026-0912', result: Number(resultValue.textContent), verdict: 'Pass' };
}
/* Signed when the result read 87.2. */
const signedHash = Audit.recordHash(record());
performed.signature = { id: 's1', userId: 'jdoe', userName: 'John Doe', meaning: 'authored', at: '2026-09-18T11:05:12.000+03:00', recordHash: signedHash };
reviewed.signature = { id: 's2', userId: 'rkim', userName: 'Rachel Kim', meaning: 'reviewed', at: '2026-09-18T13:48:30.000+03:00', reason: 'Second-person review', recordHash: signedHash };
approved.signature = { id: 's3', userId: 'asmith', userName: 'Alice Smith', meaning: 'approved', at: '2026-09-18T14:32:00.000+03:00', reason: 'Batch release', recordHash: signedHash };
function verify() {
blocks.forEach(function (block) {
block.record = record();
});
}
alter.addEventListener('change', function () {
resultValue.textContent = alter.checked ? '79.2' : '87.2';
verify();
});
compact.addEventListener('change', function () {
blocks.forEach(function (block) {
block.compact = compact.checked;
});
});
hash.addEventListener('change', function () {
blocks.forEach(function (block) {
block.showHash = hash.checked;
});
});
noId.addEventListener('change', function () {
blocks.forEach(function (block) {
block.showUserId = !noId.checked;
});
});
verify();
});
return () => {
unmounted = true;
};
}, []);
return (
<div className="react-root" ref={root} theme="industrial">
<div className="smart-demo-container">
<section id="overview">
<h2>Smart.SignatureBlock displays the signature manifestation of a signed record: the meaning of the signature, the printed name of the signer, the date and time, and whether the record is unchanged since it was signed.</h2>
<p>21 CFR Part 11 §11.50 requires a signed electronic record to show the printed name of the signer, the date and time of signing and the meaning of the signature. §11.70 requires the signature to be linked to its record so it cannot be transferred to another record. The component displays the manifestation and, when the current record is provided, compares it with the hash stored in the signature. The signature object is the one produced by <code>smart-esignature</code>.</p>
</section>
<section>
<h2>A test result with three signatures</h2>
<div className="result">
<table className="result-fields">
<tbody>
<tr>
<th>Test</th>
<td>Dissolution, 30 min</td>
</tr>
<tr>
<th>Batch</th>
<td>B-2026-0912</td>
</tr>
<tr>
<th>Result</th>
<td><span id="resultValue">87.2</span> % (limit ≥ 80 %)</td>
</tr>
<tr>
<th>Verdict</th>
<td>Pass</td>
</tr>
</tbody>
</table>
<div className="blocks">
<SignatureBlock id="performed"></SignatureBlock>
<SignatureBlock id="reviewed"></SignatureBlock>
<SignatureBlock id="approved"></SignatureBlock>
</div>
</div>
<div className="demo-actions">
<label><input type="checkbox" id="alter" /> Alter the result after signing</label>
<label><input type="checkbox" id="compact" /> Compact</label>
<label><input type="checkbox" id="hash" /> Show the record hash</label>
<label><input type="checkbox" id="noId" /> Hide user IDs</label>
</div>
<p className="hint">Each block is an accessible group named by its content, for example "Signature: Approved by Alice Smith, 2026-09-18 14:32:00 +03:00, Batch release, Record matches signature", so the verification result is available as text and not only as a colour.</p>
</section>
</div>
</div>
);
};
export default App;
react/App.css
.result {
display: grid;
grid-template-columns: minmax(260px, 420px) minmax(280px, 1fr);
gap: 24px;
align-items: start;
}
@media (max-width: 900px) {
.result {
grid-template-columns: 1fr;
}
}
.result-fields {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.result-fields th,
.result-fields td {
padding: 6px 10px;
border-bottom: 1px solid var(--smart-border);
text-align: left;
}
.result-fields th {
width: 30%;
font-weight: 600;
opacity: 0.75;
}
.blocks {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.demo-actions {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 14px;
margin-top: 16px;
}
.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.SignatureBlock displays the signature manifestation of a signed record: the meaning of the signature, the printed name of the signer, the date and time, and whether the record is unchanged since it was signed.</h2>
<p>
21 CFR Part 11 §11.50 requires a signed electronic record to show the printed name of the signer, the date and time of signing and the meaning of the signature. §11.70 requires the signature to be linked to its record so it cannot be transferred to another record. The component displays the manifestation and, when the current record is provided, compares it with the hash stored in the signature. The signature object is the one produced by <code>smart-esignature</code>.
</p>
</section>
<section>
<h2>A test result with three signatures</h2>
<div class="result">
<table class="result-fields">
<tr><th>Test</th><td>Dissolution, 30 min</td></tr>
<tr><th>Batch</th><td>B-2026-0912</td></tr>
<tr><th>Result</th><td><span id="resultValue">87.2</span> % (limit ≥ 80 %)</td></tr>
<tr><th>Verdict</th><td>Pass</td></tr>
</table>
<div class="blocks">
<smart-signature-block id="performed"></smart-signature-block>
<smart-signature-block id="reviewed"></smart-signature-block>
<smart-signature-block id="approved"></smart-signature-block>
</div>
</div>
<div class="demo-actions">
<label><input type="checkbox" id="alter" /> Alter the result after signing</label>
<label><input type="checkbox" id="compact" /> Compact</label>
<label><input type="checkbox" id="hash" /> Show the record hash</label>
<label><input type="checkbox" id="noId" /> Hide user IDs</label>
</div>
<p class="hint">
Each block is an accessible group named by its content, for example "Signature: Approved by Alice Smith, 2026-09-18 14:32:00 +03:00, Batch release, Record matches signature", so the verification result is available as text and not only as a colour.
</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.signatureblock.js";
const Smart = window.Smart;
/* Three signatures on one test result, as smart-esignature would have produced them - each
carrying the hash of the record as it was when signed. The blocks are given the record
as it is now, so altering the result turns every verdict to "altered". */
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 Audit = Smart.Industrial.Audit,
performed = document.getElementById('performed'),
reviewed = document.getElementById('reviewed'),
approved = document.getElementById('approved'),
resultValue = document.getElementById('resultValue'),
alter = document.getElementById('alter'),
compact = document.getElementById('compact'),
hash = document.getElementById('hash'),
noId = document.getElementById('noId'),
blocks = [performed, reviewed, approved];
function record() {
return { test: 'Dissolution, 30 min', batch: 'B-2026-0912', result: Number(resultValue.textContent), verdict: 'Pass' };
}
/* Signed when the result read 87.2. */
const signedHash = Audit.recordHash(record());
performed.signature = { id: 's1', userId: 'jdoe', userName: 'John Doe', meaning: 'authored', at: '2026-09-18T11:05:12.000+03:00', recordHash: signedHash };
reviewed.signature = { id: 's2', userId: 'rkim', userName: 'Rachel Kim', meaning: 'reviewed', at: '2026-09-18T13:48:30.000+03:00', reason: 'Second-person review', recordHash: signedHash };
approved.signature = { id: 's3', userId: 'asmith', userName: 'Alice Smith', meaning: 'approved', at: '2026-09-18T14:32:00.000+03:00', reason: 'Batch release', recordHash: signedHash };
function verify() {
blocks.forEach(function (block) {
block.record = record();
});
}
alter.addEventListener('change', function () {
resultValue.textContent = alter.checked ? '79.2' : '87.2';
verify();
});
compact.addEventListener('change', function () {
blocks.forEach(function (block) {
block.compact = compact.checked;
});
});
hash.addEventListener('change', function () {
blocks.forEach(function (block) {
block.showHash = hash.checked;
});
});
noId.addEventListener('change', function () {
blocks.forEach(function (block) {
block.showUserId = !noId.checked;
});
});
verify();
});
});
}
};
</script>
<style>
.result {
display: grid;
grid-template-columns: minmax(260px, 420px) minmax(280px, 1fr);
gap: 24px;
align-items: start;
}
@media (max-width: 900px) {
.result {
grid-template-columns: 1fr;
}
}
.result-fields {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.result-fields th,
.result-fields td {
padding: 6px 10px;
border-bottom: 1px solid var(--smart-border);
text-align: left;
}
.result-fields th {
width: 30%;
font-weight: 600;
opacity: 0.75;
}
.blocks {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.demo-actions {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 14px;
margin-top: 16px;
}
.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
- A test result with three signatures