Repository: github.com/christophe77/ind-ds · Storybook: christophe77.github.io/ind-ds
Industrial design system for HMI/SCADA interfaces. Web Components at the core, typed wrappers for React and Vue, ISA-18.2-aware tokens, MQTT binding for live process data.
Generic web design systems target marketing sites and SaaS dashboards. They don't ship state="fault", ISA-18.2 alarm priorities, tabular figures by default, dense 2px-stepped spacing, or runtime high-contrast mode for control rooms. ind-ds does, and it stays framework-agnostic so the same components run in a Vue Nuxt portal, a React engineering app, and an embedded panel built with Lit.
- Monorepo — pnpm workspaces + Turborepo
- Core — Stencil 4 → web components + auto-generated React/Vue wrappers
- Tokens — Style Dictionary 4 → CSS vars, ES modules, JSON, Dart (for Flutter HMIs)
- Playground — Storybook 9 (
@storybook/web-components-vite) + a11y addon + HMI viewports — live preview on GitHub Pages - Realtime —
mqtt.js, thin binding layer - Trends — uPlot (planned for
<ind-trend>) — lightweight, no React/D3 - ESM-only,
sideEffects: false(except CSS), tree-shakable, no heavy runtime deps
ind-ds/
├── package.json # workspace root, scripts
├── pnpm-workspace.yaml
├── turbo.json # build graph
├── tsconfig.base.json # shared TS config
├── packages/
│ ├── tokens/ # source of truth → CSS/JS/TS/JSON/Dart
│ ├── core/ # Stencil web components
│ │ └── src/components/
│ │ ├── atoms/ # LED, value, valve, inputs, canvases…
│ │ ├── molecules/ # health card, fill row, nav item…
│ │ ├── organisms/ # app header, sidebar, status bar…
│ │ └── templates/ # Storybook screen compositions (not published)
│ ├── react/ # generated React wrappers
│ ├── vue/ # generated Vue 3 wrappers
│ └── mqtt/ # MQTT → DOM binding
└── apps/
└── storybook/ # playground (dark/light/HC, HMI viewports)
Published packages live under the @ind-ds scope on npm.
| Package | What it ships |
|---|---|
@ind-ds/tokens |
CSS variables, ESM/TS tokens, JSON, Dart |
@ind-ds/core |
114 web components (<ind-led>, <ind-valve>, …) + loader |
@ind-ds/react |
Typed React 18+ wrappers (auto-registers elements) |
@ind-ds/vue |
Typed Vue 3 wrappers with v-model support |
@ind-ds/mqtt |
MQTT → DOM attribute binding helpers |
# Tokens only (CSS vars + themes)
npm install @ind-ds/tokens
# Web components (includes tokens as a dependency)
npm install @ind-ds/core
# Framework wrappers (pull in core automatically)
npm install @ind-ds/react # or @ind-ds/vue
# Live process data
npm install @ind-ds/mqttPeer deps:
@ind-ds/reactneedsreact+react-dom≥ 18.@ind-ds/vueneedsvue≥ 3.3.
Import the token CSS once at app boot, then flip data-theme on <html> (or any ancestor):
data-theme |
Use case |
|---|---|
(omit) or "dark" |
Control-room default |
"light" |
Daylight / office |
"high-contrast" |
WCAG AAA operator stations |
@import "@ind-ds/tokens/css";
@import "@ind-ds/tokens/css/light";
@import "@ind-ds/tokens/css/high-contrast";<html data-theme="dark">No rebuild required — switching theme is a single attribute change.
Wrappers register custom elements on first mount — no manual defineCustomElements call.
// main.tsx — load tokens + optional layout utilities
import '@ind-ds/tokens/css';
import '@ind-ds/tokens/css/light';
import '@ind-ds/tokens/css/high-contrast';
import '@ind-ds/core/css/utilities';
// App.tsx
import { IndLed, IndValue, IndAlarm, IndFillRow } from '@ind-ds/react';
export function TankRow({ level, alarm }: { level: number; alarm: 'none' | 'high' | 'high-high' }) {
return (
<div className="ind-stack" data-theme="dark" style={{ background: 'var(--ind-surface-background)', padding: 16 }}>
<IndFillRow label="Tank T-204" value={level} unit="%" severity={alarm !== 'none'} />
<IndValue value={level} unit="%" trend={level > 80 ? 'up' : 'flat'} alarm={alarm} />
{alarm !== 'none' && <IndAlarm priority={alarm} label="Level high" />}
<IndLed state={level > 90 ? 'warning' : 'running'} label="T-204" />
</div>
);
}Custom events use React-style prop names (onIndChange, onIndInput, …). See Storybook for every component.
<!-- main.ts -->
import '@ind-ds/tokens/css'
import '@ind-ds/tokens/css/light'
import '@ind-ds/tokens/css/high-contrast'
import '@ind-ds/core/css/utilities'<script setup lang="ts">
import { IndLed, IndInput, IndButton } from '@ind-ds/vue';
import { ref } from 'vue';
const tag = ref('');
</script>
<template>
<div class="ind-stack" data-theme="dark">
<IndInput v-model:value="tag" label="Tag name" placeholder="P-101" />
<IndButton variant="primary" @ind-activate="() => {}">Apply</IndButton>
<IndLed state="running" :label="tag || 'P-101'" />
</div>
</template>Elements register lazily on mount, so Nuxt SSR won't choke during render.
Register once, then use tags directly in HTML or any template language:
// app.ts
import '@ind-ds/tokens/css';
import '@ind-ds/tokens/css/light';
import '@ind-ds/tokens/css/high-contrast';
import '@ind-ds/core/css/utilities';
import { defineCustomElements } from '@ind-ds/core/loader';
defineCustomElements();<div class="ind-stack" data-theme="dark">
<ind-value value="42.6" unit="bar" trend="up" alarm="high"></ind-value>
<ind-valve state="open" orientation="horizontal" label="XV-101"></ind-valve>
<ind-led state="fault" blinking label="P-101"></ind-led>
</div>Per-component lazy loading (smaller initial bundle):
import { defineCustomElement as defineIndLed } from '@ind-ds/core/dist/components/ind-led.js';
defineIndLed();Use the design tokens without web components — Flutter HMIs, legacy SCADA shells, or a custom React chart library:
/* global.css */
@import "@ind-ds/tokens/css";import { colorStateFaultBg, spacing4, fontSizeBase } from '@ind-ds/tokens';
// or: import tokens from '@ind-ds/tokens/json' with assert { type: 'json' }// Flutter — vendor dist/dart/tokens.dart from the npm package
import 'tokens.dart';
Color fault = Color(IndTokens.IndColorPaletteRed500);Wire a broker topic to component attributes without framework-specific glue:
import { IndMqttClient, bindLed, bindBlink } from '@ind-ds/mqtt';
import { defineCustomElements } from '@ind-ds/core/loader';
defineCustomElements();
const client = new IndMqttClient({ url: 'wss://broker.example:8083/mqtt' });
await client.connect();
const pump = document.querySelector('#pump-101')!;
bindLed(client, 'plant/area1/pump-101/state', pump, (raw) =>
raw === 'RUN' ? 'running' : raw === 'FAULT' ? 'fault' : 'stopped',
);
bindBlink(client, 'plant/area1/pump-101/alarm/unack', pump);@ind-ds/core ships Stencil web components. They are Custom Elements: they need a DOM (document, HTMLElement, Shadow DOM). There is no official Qt, WPF, SwiftUI, or Android View binding today.
You have two realistic strategies:
| Strategy | When to use | What you install |
|---|---|---|
| WebView shell | Desktop (Electron, Tauri), mobile (Capacitor, Flutter WebView), embedded panel (Chromium kiosk) |
@ind-ds/core + @ind-ds/tokens — same code as a web app |
| Native UI + tokens | Flutter HMIs, Qt/QML, .NET MAUI, pure native SCADA clients | @ind-ds/tokens only — rebuild widgets in your toolkit, keep the same vocabulary |
In both cases the component contract stays the same: process states (running, stopped, fault, warning, maintenance), ISA-18.2 alarm priorities (high-high, high, low, low-low), dense spacing, tabular figures. Storybook is the visual reference for behaviour and props.
If your app already hosts HTML (or can), use the Stencil components as-is inside a WebView:
┌─────────────────────────────────────┐
│ Native shell (Electron / Tauri / │
│ Capacitor / Qt WebEngine / WKWebView)│
│ ┌───────────────────────────────┐ │
│ │ Your HMI page (HTML/JS) │ │
│ │ defineCustomElements() │ │
│ │ <ind-led> <ind-value> … │ │
│ └───────────────────────────────┘ │
│ ↕ bridge (optional) │
│ OS APIs — serial, filesystem, … │
└─────────────────────────────────────┘
Electron / Tauri renderer
// renderer.ts
import '@ind-ds/tokens/css';
import '@ind-ds/core/css/utilities';
import { defineCustomElements } from '@ind-ds/core/loader';
defineCustomElements();
// render your HMI HTML — identical to the vanilla web example aboveCapacitor (iOS / Android) — same imports in the WebView bundle; use Capacitor plugins for device APIs alongside the web UI.
Flutter webview_flutter — load a local HTML asset or remote URL that bundles @ind-ds/core; communicate with Dart via JavaScriptChannel when you need native navigation around the web panel.
@ind-ds/mqtttargets DOM nodes (element.setAttribute). In a WebView HMI it works unchanged. In a fully native UI, subscribe with your platform MQTT client and map payloads to widget state yourself (same transforms asbindLed).
Install only the token package and mirror component semantics in your UI toolkit:
npm install @ind-ds/tokens
# or in pubspec / Gradle / CocoaPods: vendor the generated files belowAvailable cross-platform exports
| Export path | Format | Typical consumer |
|---|---|---|
@ind-ds/tokens |
ESM + .d.ts |
Node tooling, bundlers |
@ind-ds/tokens/json |
flat JSON | codegen → Kotlin, Swift, C#, QML |
@ind-ds/tokens/dart |
IndTokens class |
Flutter / Dart HMIs |
@ind-ds/tokens/css |
CSS variables | WebView pages, hybrid shells |
Flutter — native LED using Dart tokens
Copy or generate from the published Dart file (node_modules/@ind-ds/tokens/dist/dart/tokens.dart or import via build script):
import 'package:flutter/material.dart';
import 'tokens.dart'; // IndTokens from @ind-ds/tokens/dart
enum ProcessState { running, stopped, fault, warning, maintenance }
Color _ledBg(ProcessState state) => switch (state) {
ProcessState.running => Color(IndTokens.IndColorPaletteGreen500),
ProcessState.stopped => Color(IndTokens.IndColorPaletteNeutral500),
ProcessState.fault => Color(IndTokens.IndColorPaletteRed500),
ProcessState.warning => Color(IndTokens.IndColorPaletteAmber500),
ProcessState.maintenance => Color(IndTokens.IndColorPaletteBlue500),
};
/// Mirrors <ind-led state="…" label="…"> — same prop vocabulary as the Stencil component.
class IndLed extends StatelessWidget {
const IndLed({super.key, required this.state, this.label});
final ProcessState state;
final String? label;
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: _ledBg(state),
shape: BoxShape.circle,
boxShadow: [BoxShadow(color: _ledBg(state).withValues(alpha: 0.6), blurRadius: 6)],
),
),
if (label != null) ...[const SizedBox(width: 6), Text(label!, style: const TextStyle(fontFeatures: [FontFeature.tabularFigures()]))],
],
);
}
}JSON → your platform (Qt, .NET, Rust…)
# vendor the flat map once per release
cp node_modules/@ind-ds/tokens/dist/json/tokens.json tooling/tokens.jsonFeed it to Style Dictionary, Tokens Studio, or an in-house codegen script to emit Theme.kt, Colors.swift, QML singletons, etc. Re-run when @ind-ds/tokens bumps.
Native alarm chip — same ISA-18.2 priority enum as <ind-alarm>
// priority: 'high-high' | 'high' | 'low' | 'low-low' (identical to the web component)
Color alarmBg(String priority) => switch (priority) {
'high-high' => Color(IndTokens.IndAlarmHighHighBg),
'high' => Color(IndTokens.IndAlarmHighBg),
'low' => Color(IndTokens.IndAlarmLowBg),
'low-low' => Color(IndTokens.IndAlarmLowLowBg),
_ => Color(IndTokens.IndColorPaletteNeutral500),
};- Need pixel-perfect parity with Storybook fast? → WebView +
@ind-ds/core. - Need offline native performance, platform widgets, or store compliance? →
@ind-ds/tokens+ hand-rolled widgets that follow the same props/states. - Mixed fleet (portal in React, rounds app in Flutter, panel in Qt)? → tokens are the shared source of truth; each shell implements UI once, all speak
running/high-high/--ind-spacing-*.
Clone and hack on the design system itself:
git clone https://github.com/christophe77/ind-ds.git
cd ind-ds
pnpm install
pnpm build # tokens → core → wrappers, via turbo
pnpm storybook # http://localhost:6006Requires Node 20+ and pnpm 9+.
114 web components ship in @ind-ds/core (53 atoms, 32 molecules, 29 organisms). Each tag has a matching React wrapper (IndLed, IndValue, …) and Vue wrapper regenerated on every Stencil build.
| Tag | Role |
|---|---|
<ind-led> |
Process-state indicator (running, stopped, fault, warning, maintenance) with optional blink |
<ind-status-dot> |
Compact status dot for dense panels |
<ind-alarm> |
ISA-18.2 alarm chip (high-high, high, low, low-low) |
<ind-badge> |
Semantic pill / state badge |
<ind-counter> |
Numeric count chip with max clamp and zero-dot mode |
<ind-signal-quality> |
Bar-style signal/quality meter |
<ind-connection-indicator> |
Connected / connecting / disconnected / error dot + label |
<ind-heartbeat> |
Liveness pulse, stops when alive=false |
<ind-sparkline> |
Lightweight trend/mini-trend polyline (no chart lib) |
<ind-progress> |
Determinate / indeterminate progress bar |
<ind-progress-ring> |
Circular progress / spinner with optional center value |
| Tag | Role |
|---|---|
<ind-valve> |
Valve symbol — open / closed / transit / fault |
<ind-pump> |
Centrifugal pump (impeller spins when running) |
<ind-motor> |
Motor with shaft + running ring |
<ind-fan> |
Fan / blower with spinning blades |
<ind-compressor> |
Centrifugal compressor |
<ind-conveyor> |
Belt conveyor with forward/reverse flow |
<ind-heater> |
Heating element (glows when running) |
<ind-cooler> |
Cooler / chiller |
<ind-tank> |
Liquid tank with level fill + alarm tint |
<ind-silo> |
Hopper-bottom silo with level fill |
<ind-pipe> |
Pipe segment with animated flow direction |
| Tag | Role |
|---|---|
<ind-button> |
Dense HMI button (primary, secondary, ghost, danger) |
<ind-input> |
Text / numeric field with label, validation, sizes |
<ind-checkbox> |
Checkbox with indeterminate state |
<ind-select> |
Dropdown with typed options |
<ind-textarea> |
Multiline input |
<ind-toggle> |
On/off switch with optional in-track text |
<ind-selector-switch> |
Multi-position selector (OFF / HAND / AUTO) |
<ind-estop> |
Latching emergency-stop mushroom button |
<ind-setpoint> |
Setpoint editor with PV compare + step buttons |
<ind-slider> |
Range slider with value/unit readout |
<ind-knob> |
Rotary knob (drag + keyboard) |
<ind-datetime-picker> |
Date / time / datetime field |
| Tag | Role |
|---|---|
<ind-icon> |
Built-in HMI icon set + custom-SVG slot |
<ind-nav-item> |
Sidebar / rail navigation entry (also a molecule export) |
<ind-breadcrumb-item> |
Breadcrumb crumb with separator + current state |
<ind-tab> |
Tab with icon/badge slots |
<ind-tree-node> |
Expandable tree node with indent + selection |
| Tag | Role |
|---|---|
<ind-value> |
Tabular numeric readout — value, unit, trend, ISA alarm severity |
<ind-label> |
Text label / caption with tone + required marker |
<ind-unit-label> |
Engineering-unit text |
<ind-timestamp> |
Formatted time (datetime / date / time / relative / iso) |
<ind-tag-name> |
Monospace equipment/instrument tag, optional boxed |
<ind-alarm-count> |
ISA-priority alarm-count chips (HH / H / L / LL) |
| Tag | Role |
|---|---|
<ind-gauge> |
Radial gauge with colored zones + needle |
<ind-linear-gauge> |
Horizontal/vertical gauge with zones + setpoint marker |
<ind-xy-point> |
XY operating point with optional trail |
<ind-process-symbol> |
ISA-5.1 instrument balloon (circle / square / diamond / hexagon) |
<ind-canvas-layer> |
Absolute-positioned layer for composing mimic diagrams |
<ind-scara-canvas> |
2D top-down SCARA arm — joint angles → SVG forward kinematics |
<ind-shelf-canvas> |
Rack of resupplyable slots with per-slot fill level |
<ind-divider> |
Horizontal or vertical rule |
<ind-dialog> |
Modal dialog shell |
| Tag | Role |
|---|---|
<ind-health-card> |
Equipment health summary — state, metrics, optional sparkline slot |
<ind-fill-row> |
Label + horizontal fill bar (tank level, capacity, severity tint) |
<ind-nav-item> |
Sidebar / rail navigation entry with icon and active state |
<ind-toolbar-action> |
Toolbar action cluster with optional message counter |
| Tag | Role |
|---|---|
<ind-tag-card> |
Single process tag — identifier, live value and status dot |
<ind-equipment-status-card> |
Generic equipment status with a slotted process symbol + state badge |
<ind-motor-card> |
Motor faceplate — symbol + speed / current / load readouts |
<ind-pump-card> |
Pump faceplate — symbol + flow / pressure readouts |
<ind-valve-card> |
Valve faceplate — symbol + open/closed state + position |
<ind-tank-level-card> |
Tank faceplate — symbol + level value + linear gauge |
<ind-alarm-badge-group> |
ISA-18.2 priority counters (HH / H / L / LL) with optional total |
<ind-kpi-card> |
Headline KPI — large value, delta and trend arrow |
<ind-energy-card> |
Power / energy KPI with sparkline and cumulative total |
| Tag | Role |
|---|---|
<ind-setpoint-control> |
Labelled setpoint faceplate (SP vs PV) |
<ind-start-stop-control> |
Start / stop command pair with running LED |
<ind-speed-control> |
Speed / rate control (slider or knob) |
<ind-temperature-control> |
Temperature loop — setpoint, PV and heat/cool indicator |
<ind-mode-selector> |
Operating-mode selector (Off / Manual / Auto) |
<ind-recipe-selector> |
Recipe picker with Load command |
<ind-batch-parameter-row> |
Editable batch / recipe parameter row |
| Tag | Role |
|---|---|
<ind-sidebar-group> |
Collapsible sidebar section heading for nav items |
<ind-command-group> |
Segmented / attached cluster of command buttons |
| Tag | Role |
|---|---|
<ind-trend-widget> |
Compact trend — value + sparkline + min/max bounds |
<ind-alarm-row> |
Alarm list line — priority, tag, message, time, acknowledge |
<ind-event-row> |
Event log line — time, severity dot, source, message |
<ind-audit-row> |
Audit-trail line — time, user, action, detail |
<ind-historical-value-row> |
Historical sample — time, value, OPC data-quality flag |
<ind-device-info-card> |
Device nameplate — vendor / model / firmware / serial + connection |
| Tag | Role |
|---|---|
<ind-mqtt-tag-binding> |
MQTT topic binding — value, QoS, retain, broker state |
<ind-opcua-tag-binding> |
OPC UA node binding — value, data quality, session state |
<ind-device-connection-card> |
Driver connection — protocol, endpoint, state, latency |
<ind-network-status-card> |
Network link — signal quality, address, throughput, state |
| Tag | Role |
|---|---|
<ind-app-header> |
Top chrome — title, connection state, user slot |
<ind-sidebar-nav> |
Collapsible sidebar with grouped ind-nav-item children |
<ind-status-bar> |
Bottom status strip — alarms, clock, connection, custom slots |
<ind-mqtt-monitor> |
Live MQTT panel — broker status, topic list, message stream |
<ind-context-toolbar> |
Contextual toolbar — title / breadcrumb + contextual actions |
<ind-command-center> |
Grid palette of command buttons / groups |
| Tag | Role |
|---|---|
<ind-alarm-panel> |
Active alarm list (unacked-first, ISA sorted) with ACK / ACK-all |
<ind-alarm-summary> |
Alarm KPI block — total / unacked + priority breakdown |
<ind-event-journal> |
Event log with severity filter |
<ind-historian-viewer> |
Single-tag history table + sparkline overview |
<ind-trend-viewer> |
Multi-series sparkline trend viewer with legend |
<ind-equipment-dashboard> |
Responsive grid host for equipment faceplates |
<ind-production-dashboard> |
KPI / tag / trend dashboard grid |
<ind-energy-dashboard> |
Energy cards grid with headline total |
| Tag | Role |
|---|---|
<ind-process-control-panel> |
Generic loop — run command, setpoint vs PV, mode |
<ind-motor-control-panel> |
Motor faceplate + start/stop + speed + mode |
<ind-pump-control-panel> |
Pump faceplate + start/stop + mode |
<ind-valve-control-panel> |
Valve faceplate + Open/Close/Stop + position |
<ind-batch-control-panel> |
Phase + progress + Start/Hold/Stop + parameters |
<ind-recipe-management-panel> |
Recipe pick/load + parameter set + save |
| Tag | Role |
|---|---|
<ind-asset-overview> |
Asset register — state, health bar, detail |
<ind-maintenance-dashboard> |
MTBF / MTTR / WO KPIs + due-task list |
<ind-device-diagnostics-panel> |
Nameplate + connection + diagnostic metrics |
<ind-firmware-update-panel> |
Version compare + progress + Check / Install |
| Tag | Role |
|---|---|
<ind-production-line-overview> |
Station sequence with flow arrows |
<ind-machine-overview> |
Machine header (state + OEE) + equipment grid |
<ind-production-cell> |
Work-cell container grouping machines / equipment |
<ind-workstation-monitor> |
Operator station — job, produced/rejected, target |
<ind-oee-dashboard> |
OEE headline + Availability / Performance / Quality gauges |
| Tag | Status |
|---|---|
<ind-trend> |
Full time-series trend (uPlot) — not shipped yet. For lightweight inline trends use <ind-sparkline>. |
Storybook templates (connection, connected-app) compose the above into full-screen mocks — they are documentation-only, not npm exports.
Per-component lazy import:
import { defineCustomElement as defineIndLed } from '@ind-ds/core/dist/components/ind-led.js';
defineIndLed();@ind-ds/tokens— process states, ISA-18.2 alarm priorities, dense spacing, tabular figures, dark / light / high-contrast themes. CSS variables, ESM, JSON, Dart.@ind-ds/core— Stencil web components listed above +defineCustomElementsloader and layout utilities CSS.@ind-ds/react— auto-generated typedforwardRefwrappers (IndLed,IndValue,IndAlarm, …) for every tag.@ind-ds/vue—defineComponentwrappers with fullv-modeland event forwarding.@ind-ds/mqtt—IndMqttClient,bindLed,bindBlink, and genericbind()/TopicBindingfor any attribute.@ind-ds/storybook— component playground with HMI viewports and theme toolbar (not published).
Walk-through for adding <ind-valve>:
-
Tokens (only if a new role is needed). If
valveintroduces colors or motion not covered by existing semantic tokens, add them topackages/tokens/tokens/semantic/. Rebuild:pnpm --filter @ind-ds/tokens build. -
Stencil component. Create:
packages/core/src/components/atoms/valve/ valve.tsx valve.cssUse the LED component as the template. Tag is
ind-valve, shadow DOM on,@Prop({ reflect: true })for everything that participates in the CSS selectors, every animation guarded byprefers-reduced-motion. -
Re-export the types from
packages/core/src/index.ts. -
Build core.
pnpm --filter @ind-ds/core build. This:- generates
src/components.d.ts - bundles the custom element
- regenerates React and Vue wrappers (once
@stencil/react-output-targetand@stencil/vue-output-targetare wired — see each wrapper README)
- generates
-
Stories. Add
apps/storybook/stories/atoms/valve.stories.ts, mirroringled.stories.ts. Always include the "AllStates" gallery and at least one realistic dense-panel mock. -
MQTT (optional). If the component has a canonical binding pattern, add a
bindValveconvenience topackages/mqtt/src/index.ts.
- Semantic over decorative. Components consume
--ind-state-*and--ind-alarm-*tokens, never raw palette tokens. Theming is free. - HMI density first. 12px base font, 2px-stepped spacing, no rounded-corner softness. Operators have 24-inch screens packed with values; whitespace is hostile.
- ISA-18.2 throughout. Alarm priorities are a token group with explicit priority numbers and labels — no ad-hoc severity scales.
- Accessibility is non-negotiable. Live regions, visible focus,
prefers-reduced-motion, WCAG AAA via high-contrast theme. A control room ops 24/7; the design system must respect that. - Framework wrappers are derived. Stencil is the single source of truth. React and Vue ship typed proxies regenerated on every build — no parallel maintenance.
MIT — see LICENSE.