@devicechain/widgets 0.21.1-rc.1 → 0.22.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +28 -7
  2. package/dist/aggregate-feed.d.ts +14 -0
  3. package/dist/aggregate-fold.d.ts +11 -0
  4. package/dist/aggregate-reading.d.ts +19 -0
  5. package/dist/aggregate-strings.d.ts +16 -0
  6. package/dist/binding-policy.d.ts +3 -1
  7. package/dist/builtin-descriptors.d.ts +1210 -8
  8. package/dist/builtin-registry.d.ts +1197 -8
  9. package/dist/category-geometry.d.ts +19 -0
  10. package/dist/category-items.d.ts +10 -0
  11. package/dist/chunks/aggregate-feed-6ZYKOQOB.js +130 -0
  12. package/dist/chunks/aggregate-feed-6ZYKOQOB.js.map +7 -0
  13. package/dist/chunks/aggregate-widgets-WVXAW3UL.js +895 -0
  14. package/dist/chunks/aggregate-widgets-WVXAW3UL.js.map +7 -0
  15. package/dist/chunks/chunk-2D7JNKFE.js +65 -0
  16. package/dist/chunks/chunk-2D7JNKFE.js.map +7 -0
  17. package/dist/chunks/chunk-S2OMBJLI.js +200 -0
  18. package/dist/chunks/chunk-S2OMBJLI.js.map +7 -0
  19. package/dist/chunks/chunk-XQQQNTBX.js +76 -0
  20. package/dist/chunks/chunk-XQQQNTBX.js.map +7 -0
  21. package/dist/chunks/chunk-YPFB6DAL.js +157 -0
  22. package/dist/chunks/chunk-YPFB6DAL.js.map +7 -0
  23. package/dist/chunks/chunk-ZR74ORIT.js +35 -0
  24. package/dist/chunks/chunk-ZR74ORIT.js.map +7 -0
  25. package/dist/chunks/control-widgets-UDS4ET3T.js +775 -0
  26. package/dist/chunks/control-widgets-UDS4ET3T.js.map +7 -0
  27. package/dist/chunks/fleet-feed-FK4ZMKIK.js +252 -0
  28. package/dist/chunks/fleet-feed-FK4ZMKIK.js.map +7 -0
  29. package/dist/chunks/level-widgets-MJB7TOLU.js +833 -0
  30. package/dist/chunks/level-widgets-MJB7TOLU.js.map +7 -0
  31. package/dist/command-control-core.d.ts +68 -0
  32. package/dist/command-control-strings.d.ts +32 -0
  33. package/dist/command-control.d.ts +31 -0
  34. package/dist/dashboard-renderer.d.ts +15 -4
  35. package/dist/fleet-feed.d.ts +21 -0
  36. package/dist/fleet-model.d.ts +43 -0
  37. package/dist/fleet-seed.d.ts +7 -0
  38. package/dist/fleet-strings.d.ts +25 -0
  39. package/dist/heading.d.ts +3 -0
  40. package/dist/index.d.ts +3 -2
  41. package/dist/index.js +1774 -676
  42. package/dist/index.js.map +4 -4
  43. package/dist/level-reading.d.ts +29 -0
  44. package/dist/level-readout.d.ts +25 -0
  45. package/dist/level-strings.d.ts +13 -0
  46. package/dist/options.d.ts +1 -1
  47. package/dist/time-range-bar.d.ts +28 -0
  48. package/dist/use-held.d.ts +1 -0
  49. package/dist/value-scale.d.ts +30 -0
  50. package/dist/widgets/aggregate-lazy.d.ts +305 -0
  51. package/dist/widgets/aggregate-widgets.d.ts +4 -0
  52. package/dist/widgets/alarm-count.d.ts +1 -0
  53. package/dist/widgets/alarm-table.d.ts +4 -1
  54. package/dist/widgets/bar-chart.d.ts +5 -0
  55. package/dist/widgets/battery.d.ts +7 -0
  56. package/dist/widgets/command-slider.d.ts +2 -0
  57. package/dist/widgets/command-toggle.d.ts +4 -0
  58. package/dist/widgets/compass.d.ts +2 -0
  59. package/dist/widgets/control-lazy.d.ts +92 -0
  60. package/dist/widgets/control-widgets.d.ts +2 -0
  61. package/dist/widgets/fleet-table.d.ts +13 -0
  62. package/dist/widgets/gauge.d.ts +52 -0
  63. package/dist/widgets/label.d.ts +1 -1
  64. package/dist/widgets/latest-card.d.ts +44 -1
  65. package/dist/widgets/lazy-chunk.d.ts +8 -0
  66. package/dist/widgets/level-gauge.d.ts +2 -0
  67. package/dist/widgets/level-lazy.d.ts +647 -0
  68. package/dist/widgets/level-widgets.d.ts +7 -0
  69. package/dist/widgets/linear-gauge.d.ts +2 -0
  70. package/dist/widgets/pie-chart.d.ts +2 -0
  71. package/dist/widgets/progress-ring.d.ts +3 -0
  72. package/dist/widgets/severity.d.ts +2 -1
  73. package/dist/widgets/signal-bars.d.ts +3 -0
  74. package/dist/widgets/stat-card.d.ts +3 -0
  75. package/dist/widgets/status-light.d.ts +7 -0
  76. package/dist/widgets/table.d.ts +9 -1
  77. package/dist/widgets/time-series-chart.d.ts +62 -4
  78. package/package.json +8 -8
@@ -0,0 +1,76 @@
1
+ // src/level-readout.tsx
2
+ import {
3
+ css,
4
+ fitValueFont,
5
+ fitValueFontCss,
6
+ FIT_MIN_FONT_PX,
7
+ fontVar,
8
+ spaceVar,
9
+ useStickyChars
10
+ } from "@devicechain/widget-sdk";
11
+ import { jsx, jsxs } from "react/jsx-runtime";
12
+ var MARKER = { above: "\u25B2", below: "\u25BC" };
13
+ function LevelReadout({ text, unit, color, position, size, scale, strings }) {
14
+ const hasUnit = unit !== void 0 && unit !== "";
15
+ const marked = position === "above" || position === "below";
16
+ const chars = useStickyChars(text.length + (hasUnit ? Math.ceil(unit.length / 2) + 1 : 0) + (marked ? 1 : 0));
17
+ const floor = Math.round(FIT_MIN_FONT_PX * scale);
18
+ const measured = size.width > 0 && size.height > 0;
19
+ const px = measured ? fitValueFont(size, chars, { floor }) : void 0;
20
+ const font = px ?? fitValueFontCss(chars, { floor });
21
+ const half = px !== void 0 ? Math.round(px / 2) : `calc(${font} / 2)`;
22
+ return /* @__PURE__ */ jsxs(
23
+ "div",
24
+ {
25
+ "data-level-readout": "",
26
+ style: {
27
+ fontSize: font,
28
+ fontWeight: 600,
29
+ lineHeight: 1.1,
30
+ whiteSpace: "nowrap",
31
+ fontVariantNumeric: "tabular-nums",
32
+ color: color ?? css("foreground"),
33
+ textAlign: "center"
34
+ },
35
+ children: [
36
+ text,
37
+ hasUnit ? /* @__PURE__ */ jsx("span", { style: { fontSize: half, color: css("muted-foreground"), marginLeft: spaceVar(1) }, children: unit }) : null,
38
+ marked ? /* @__PURE__ */ jsx(
39
+ "span",
40
+ {
41
+ role: "img",
42
+ "aria-label": position === "above" ? strings.aboveScale : strings.belowScale,
43
+ title: position === "above" ? strings.aboveScale : strings.belowScale,
44
+ style: { fontSize: half, color: css("muted-foreground"), marginLeft: spaceVar(1) },
45
+ children: MARKER[position]
46
+ }
47
+ ) : null
48
+ ]
49
+ }
50
+ );
51
+ }
52
+ function LevelEmpty({ message }) {
53
+ return /* @__PURE__ */ jsx(
54
+ "div",
55
+ {
56
+ "data-level-empty": "",
57
+ style: {
58
+ display: "flex",
59
+ alignItems: "center",
60
+ justifyContent: "center",
61
+ height: "100%",
62
+ textAlign: "center",
63
+ color: css("muted-foreground"),
64
+ fontSize: fontVar("sm"),
65
+ overflowWrap: "anywhere"
66
+ },
67
+ children: message
68
+ }
69
+ );
70
+ }
71
+
72
+ export {
73
+ LevelReadout,
74
+ LevelEmpty
75
+ };
76
+ //# sourceMappingURL=chunk-XQQQNTBX.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/level-readout.tsx"],
4
+ "sourcesContent": ["// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// The fitted DOM readout and the explained empty state the level-and-indicator widgets share.\n// DOM, not canvas text, so the readability gate can measure it.\n\nimport {\n css,\n fitValueFont,\n fitValueFontCss,\n FIT_MIN_FONT_PX,\n fontVar,\n spaceVar,\n useStickyChars,\n} from '@devicechain/widget-sdk';\n\nimport type { LevelStrings } from './level-strings';\nimport type { ScalePosition } from './value-scale';\n\n// The room the readout may fill (a box inside the frame's content column) and the density scale.\nexport interface ReadoutRoom {\n size: { width: number; height: number };\n scale: number;\n}\n\n// The words the off-scale marker is labelled with; the only strings the readout itself reads.\nexport type MarkerStrings = Pick<LevelStrings, 'aboveScale' | 'belowScale'>;\n\n// A readout that can be marked off-scale must be handed the marker's label, so an unlabelled\n// marker cannot be written; one that is never marked (the gauge) needs no strings at all, which\n// keeps the level-strings table out of an eager widget's bundle.\nexport type LevelReadoutProps = ReadoutRoom & {\n text: string;\n unit?: string;\n color?: string;\n} & (\n | {\n // Set when the value lies off the scale: a marker is drawn beside it.\n position?: ScalePosition;\n strings: MarkerStrings;\n }\n | { position?: undefined; strings?: undefined }\n );\n\nconst MARKER = { above: '\u25B2', below: '\u25BC' } as const;\n\nexport function LevelReadout({ text, unit, color, position, size, scale, strings }: LevelReadoutProps) {\n const hasUnit = unit !== undefined && unit !== '';\n const marked = position === 'above' || position === 'below';\n // The unit and the marker render at half size, so each counts for half its characters.\n const chars = useStickyChars(text.length + (hasUnit ? Math.ceil(unit.length / 2) + 1 : 0) + (marked ? 1 : 0));\n const floor = Math.round(FIT_MIN_FONT_PX * scale);\n const measured = size.width > 0 && size.height > 0;\n const px = measured ? fitValueFont(size, chars, { floor }) : undefined;\n const font = px ?? fitValueFontCss(chars, { floor });\n const half = px !== undefined ? Math.round(px / 2) : `calc(${font} / 2)`;\n return (\n <div\n data-level-readout=\"\"\n style={{\n fontSize: font,\n fontWeight: 600,\n lineHeight: 1.1,\n whiteSpace: 'nowrap',\n fontVariantNumeric: 'tabular-nums',\n color: color ?? css('foreground'),\n textAlign: 'center',\n }}\n >\n {text}\n {hasUnit ? (\n <span style={{ fontSize: half, color: css('muted-foreground'), marginLeft: spaceVar(1) }}>{unit}</span>\n ) : null}\n {marked ? (\n <span\n role=\"img\"\n aria-label={position === 'above' ? strings.aboveScale : strings.belowScale}\n title={position === 'above' ? strings.aboveScale : strings.belowScale}\n style={{ fontSize: half, color: css('muted-foreground'), marginLeft: spaceVar(1) }}\n >\n {MARKER[position]}\n </span>\n ) : null}\n </div>\n );\n}\n\n// Why there is nothing to draw: a short sentence in the widget's own box, never a fake value.\nexport function LevelEmpty({ message }: { message: string }) {\n return (\n <div\n data-level-empty=\"\"\n style={{\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n height: '100%',\n textAlign: 'center',\n color: css('muted-foreground'),\n fontSize: fontVar('sm'),\n overflowWrap: 'anywhere',\n }}\n >\n {message}\n </div>\n );\n}\n"],
5
+ "mappings": ";AAMA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AA2CH,SAcI,KAdJ;AAbJ,IAAM,SAAS,EAAE,OAAO,UAAK,OAAO,SAAI;AAEjC,SAAS,aAAa,EAAE,MAAM,MAAM,OAAO,UAAU,MAAM,OAAO,QAAQ,GAAsB;AACrG,QAAM,UAAU,SAAS,UAAa,SAAS;AAC/C,QAAM,SAAS,aAAa,WAAW,aAAa;AAEpD,QAAM,QAAQ,eAAe,KAAK,UAAU,UAAU,KAAK,KAAK,KAAK,SAAS,CAAC,IAAI,IAAI,MAAM,SAAS,IAAI,EAAE;AAC5G,QAAM,QAAQ,KAAK,MAAM,kBAAkB,KAAK;AAChD,QAAM,WAAW,KAAK,QAAQ,KAAK,KAAK,SAAS;AACjD,QAAM,KAAK,WAAW,aAAa,MAAM,OAAO,EAAE,MAAM,CAAC,IAAI;AAC7D,QAAM,OAAO,MAAM,gBAAgB,OAAO,EAAE,MAAM,CAAC;AACnD,QAAM,OAAO,OAAO,SAAY,KAAK,MAAM,KAAK,CAAC,IAAI,QAAQ,IAAI;AACjE,SACE;AAAA,IAAC;AAAA;AAAA,MACC,sBAAmB;AAAA,MACnB,OAAO;AAAA,QACL,UAAU;AAAA,QACV,YAAY;AAAA,QACZ,YAAY;AAAA,QACZ,YAAY;AAAA,QACZ,oBAAoB;AAAA,QACpB,OAAO,SAAS,IAAI,YAAY;AAAA,QAChC,WAAW;AAAA,MACb;AAAA,MAEC;AAAA;AAAA,QACA,UACC,oBAAC,UAAK,OAAO,EAAE,UAAU,MAAM,OAAO,IAAI,kBAAkB,GAAG,YAAY,SAAS,CAAC,EAAE,GAAI,gBAAK,IAC9F;AAAA,QACH,SACC;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,cAAY,aAAa,UAAU,QAAQ,aAAa,QAAQ;AAAA,YAChE,OAAO,aAAa,UAAU,QAAQ,aAAa,QAAQ;AAAA,YAC3D,OAAO,EAAE,UAAU,MAAM,OAAO,IAAI,kBAAkB,GAAG,YAAY,SAAS,CAAC,EAAE;AAAA,YAEhF,iBAAO,QAAQ;AAAA;AAAA,QAClB,IACE;AAAA;AAAA;AAAA,EACN;AAEJ;AAGO,SAAS,WAAW,EAAE,QAAQ,GAAwB;AAC3D,SACE;AAAA,IAAC;AAAA;AAAA,MACC,oBAAiB;AAAA,MACjB,OAAO;AAAA,QACL,SAAS;AAAA,QACT,YAAY;AAAA,QACZ,gBAAgB;AAAA,QAChB,QAAQ;AAAA,QACR,WAAW;AAAA,QACX,OAAO,IAAI,kBAAkB;AAAA,QAC7B,UAAU,QAAQ,IAAI;AAAA,QACtB,cAAc;AAAA,MAChB;AAAA,MAEC;AAAA;AAAA,EACH;AAEJ;",
6
+ "names": []
7
+ }
@@ -0,0 +1,157 @@
1
+ // src/widgets/command-status.ts
2
+ import { isTerminalCommandStatus } from "@devicechain/dashboards";
3
+ var STATUS_COLORS = {
4
+ SUCCESSFUL: "#16a34a",
5
+ // green-600 — acknowledged by the device
6
+ FAILED: "#dc2626",
7
+ // red-600
8
+ TIMEOUT: "#dc2626",
9
+ // red-600 — never acknowledged in time
10
+ EXPIRED: "#64748b",
11
+ // slate-500 — lapsed before it was ever sent
12
+ CANCELLED: "#7c3aed",
13
+ // violet-600 — deliberately called off; a decision, not a failure
14
+ QUEUED: "#0284c7",
15
+ // sky-600 — in flight
16
+ SENT: "#0284c7",
17
+ HELD: "#d97706",
18
+ // amber-600 — withheld: the device is absent, the command still stands
19
+ PARKED: "#f59e0b"
20
+ // amber-500 — published, nobody home; held for the device's return
21
+ };
22
+ var UNKNOWN_COLOR = "#64748b";
23
+ function commandStatusColor(status) {
24
+ return STATUS_COLORS[status] ?? UNKNOWN_COLOR;
25
+ }
26
+ function isTerminalStatus(status) {
27
+ return isTerminalCommandStatus(status);
28
+ }
29
+ function commandStatusLabel(status) {
30
+ if (!status) return "";
31
+ return status.charAt(0).toUpperCase() + status.slice(1).toLowerCase();
32
+ }
33
+
34
+ // src/widgets/command-params.ts
35
+ var DATA_TYPES = /* @__PURE__ */ new Set(["DOUBLE", "INT", "BOOLEAN", "STRING"]);
36
+ function parseParameterSchema(schema) {
37
+ if (!schema) return [];
38
+ let parsed;
39
+ try {
40
+ parsed = JSON.parse(schema);
41
+ } catch {
42
+ return [];
43
+ }
44
+ if (!Array.isArray(parsed)) return [];
45
+ const out = [];
46
+ for (const raw of parsed) {
47
+ const norm = normalizeParam(raw);
48
+ if (norm) out.push(norm);
49
+ }
50
+ return out;
51
+ }
52
+ function normalizeParam(raw) {
53
+ if (typeof raw !== "object" || raw === null) return null;
54
+ const r = raw;
55
+ if (typeof r.name !== "string") return null;
56
+ const param = { name: r.name };
57
+ if (typeof r.description === "string") param.description = r.description;
58
+ if (r.kind === "OBJECT") param.kind = "OBJECT";
59
+ else if (r.kind === "SCALAR") param.kind = "SCALAR";
60
+ if (typeof r.dataType === "string" && DATA_TYPES.has(r.dataType)) {
61
+ param.dataType = r.dataType;
62
+ }
63
+ if (typeof r.unit === "string") param.unit = r.unit;
64
+ if (r.required === true) param.required = true;
65
+ if (typeof r.default === "string") param.default = r.default;
66
+ if (typeof r.minValue === "number" && Number.isFinite(r.minValue)) param.minValue = r.minValue;
67
+ if (typeof r.maxValue === "number" && Number.isFinite(r.maxValue)) param.maxValue = r.maxValue;
68
+ if (Array.isArray(r.enum)) {
69
+ const values = r.enum.filter((v) => typeof v === "string");
70
+ if (values.length > 0) param.enum = values;
71
+ }
72
+ return param;
73
+ }
74
+ function isScalar(param) {
75
+ return param.kind !== "OBJECT";
76
+ }
77
+ function parseBool(raw) {
78
+ const s = raw.trim().toLowerCase();
79
+ return s === "true" || s === "1" || s === "t" || s === "yes" || s === "on";
80
+ }
81
+ function defaultValues(params) {
82
+ const values = {};
83
+ for (const p of params) {
84
+ if (!isScalar(p)) continue;
85
+ if (p.dataType === "BOOLEAN") values[p.name] = parseBool(p.default ?? "") ? "true" : "false";
86
+ else if (p.default != null) values[p.name] = p.default;
87
+ }
88
+ return values;
89
+ }
90
+ function validateParams(params, values) {
91
+ const errors = {};
92
+ for (const p of params) {
93
+ if (!isScalar(p)) {
94
+ if (p.required) errors[p.name] = "Structured parameter \u2014 not supported here";
95
+ continue;
96
+ }
97
+ if (p.dataType === "BOOLEAN") continue;
98
+ const raw = (values[p.name] ?? "").trim();
99
+ if (raw === "") {
100
+ if (p.required) errors[p.name] = "Required";
101
+ continue;
102
+ }
103
+ if (p.dataType === "INT" || p.dataType === "DOUBLE") {
104
+ const n = Number(raw);
105
+ if (!Number.isFinite(n)) {
106
+ errors[p.name] = "Must be a number";
107
+ } else if (p.dataType === "INT" && !Number.isInteger(n)) {
108
+ errors[p.name] = "Must be a whole number";
109
+ } else if (p.minValue != null && n < p.minValue) {
110
+ errors[p.name] = `Must be \u2265 ${p.minValue}`;
111
+ } else if (p.maxValue != null && n > p.maxValue) {
112
+ errors[p.name] = `Must be \u2264 ${p.maxValue}`;
113
+ }
114
+ }
115
+ }
116
+ return errors;
117
+ }
118
+ function buildPayload(params, values) {
119
+ const payload = {};
120
+ for (const p of params) {
121
+ if (!isScalar(p)) continue;
122
+ const raw = (values[p.name] ?? "").trim();
123
+ if (p.dataType === "BOOLEAN") {
124
+ payload[p.name] = parseBool(raw);
125
+ continue;
126
+ }
127
+ if (raw === "") continue;
128
+ payload[p.name] = coerce(p, raw);
129
+ }
130
+ return Object.keys(payload).length === 0 ? void 0 : JSON.stringify(payload);
131
+ }
132
+ function coerce(param, raw) {
133
+ switch (param.dataType) {
134
+ case "INT":
135
+ case "DOUBLE": {
136
+ const n = Number(raw);
137
+ return Number.isFinite(n) ? n : raw;
138
+ }
139
+ case "BOOLEAN":
140
+ return parseBool(raw);
141
+ default:
142
+ return raw;
143
+ }
144
+ }
145
+
146
+ export {
147
+ commandStatusColor,
148
+ isTerminalStatus,
149
+ commandStatusLabel,
150
+ parseParameterSchema,
151
+ isScalar,
152
+ parseBool,
153
+ defaultValues,
154
+ validateParams,
155
+ buildPayload
156
+ };
157
+ //# sourceMappingURL=chunk-YPFB6DAL.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/widgets/command-status.ts", "../../src/widgets/command-params.ts"],
4
+ "sourcesContent": ["// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// Command delivery-status presentation for the command-button widget (command-delivery\n// lifecycle: QUEUED \u2192 SENT \u2192 SUCCESSFUL | FAILED, with HELD for a dispatch withheld from\n// a known-absent device, PARKED for one published to a device that turned out to be\n// unreachable, and TIMEOUT / EXPIRED / CANCELLED as the other terminals).\n// Status color is SEMANTIC (good/working/waiting/bad), deliberately fixed rather than\n// derived from the dashboard accent, so the same status reads the same on every theme \u2014\n// and it mirrors the console's DeviceCommandsPanel status variants (success /\n// destructive / outline / in-flight) so an operator reads the same signal on both\n// surfaces.\n\nimport { isTerminalCommandStatus } from '@devicechain/dashboards';\n\n// \uD83D\uDD34 EVERY STATUS NEEDS AN ENTRY, INCLUDING THE ONES THAT LOOK NEUTRAL. UNKNOWN_COLOR\n// below is the same slate as EXPIRED, so a status with no entry does not merely fall\n// back \u2014 it becomes INDISTINGUISHABLE from \"lapsed before it was sent\". HELD and PARKED\n// (the command is still coming) rendered in EXPIRED's slate tell an operator the opposite\n// of the truth, which is why they, and CANCELLED, are spelled out rather than left to the\n// fallback.\n//\n// The scheme is by MEANING, not by lifecycle position: sky = moving under its own steam,\n// amber = waiting on a device that isn't there. HELD and PARKED share the amber family\n// because they are the same answer to \"why hasn't it arrived?\" \u2014 the device is away \u2014 and\n// take different shades because they are different answers to \"what did the platform\n// already try?\": HELD never left the platform, PARKED was published and found nobody home.\nconst STATUS_COLORS: Record<string, string> = {\n SUCCESSFUL: '#16a34a', // green-600 \u2014 acknowledged by the device\n FAILED: '#dc2626', // red-600\n TIMEOUT: '#dc2626', // red-600 \u2014 never acknowledged in time\n EXPIRED: '#64748b', // slate-500 \u2014 lapsed before it was ever sent\n CANCELLED: '#7c3aed', // violet-600 \u2014 deliberately called off; a decision, not a failure\n QUEUED: '#0284c7', // sky-600 \u2014 in flight\n SENT: '#0284c7',\n HELD: '#d97706', // amber-600 \u2014 withheld: the device is absent, the command still stands\n PARKED: '#f59e0b', // amber-500 \u2014 published, nobody home; held for the device's return\n};\n\nconst UNKNOWN_COLOR = '#64748b';\n\n// commandStatusColor maps a status to its badge color, falling back to muted slate for\n// an unrecognized value (a future/hand-edited status).\nexport function commandStatusColor(status: string): string {\n return STATUS_COLORS[status] ?? UNKNOWN_COLOR;\n}\n\n// isTerminalStatus reports whether a command has reached a state it can't leave \u2014 the\n// widget stops treating it as in-flight.\n//\n// It is NOT the question \"can this still be cancelled?\": SENT is non-terminal and is not\n// cancellable, so a host gating a cancel control must ask @devicechain/dashboards'\n// isCancellableCommandStatus instead of negating this.\n//\n// It is an alias, not a second definition: the terminal set lives in @devicechain/dashboards\n// so the console, this package and the preview cannot disagree about it. The name is kept\n// because it is part of this package's published surface.\nexport function isTerminalStatus(status: string): boolean {\n return isTerminalCommandStatus(status);\n}\n\n// commandStatusLabel renders a status in Title Case ('SUCCESSFUL' \u2192 'Successful').\nexport function commandStatusLabel(status: string): string {\n if (!status) return '';\n return status.charAt(0).toUpperCase() + status.slice(1).toLowerCase();\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// Pure form logic for the command-button widget's typed parameter form (ADR-043). The\n// console bakes a command definition's `parameterSchema` (a JSON string) into the\n// widget's options; these helpers parse it, seed defaults, validate operator input, and\n// serialize the typed values back to the JSON payload command-delivery sends the device.\n// Kept separate from the widget so the coercion/validation is unit-tested in isolation.\n//\n// SCOPE: the form types top-level SCALAR parameters (DOUBLE / INT / BOOLEAN / STRING,\n// with enum / bounds / required / default). OBJECT (nested) parameters are not\n// form-editable \u2014 the form blocks sending when one is REQUIRED (it can't be satisfied\n// here) and otherwise omits it. The payload is a flat JSON object keyed by parameter name.\n\nimport type { CommandParameter } from '@devicechain/dashboards';\n\nconst DATA_TYPES = new Set(['DOUBLE', 'INT', 'BOOLEAN', 'STRING']);\n\n// parseParameterSchema turns the baked JSON string into descriptors, defensively:\n// a null/empty/malformed schema (or a non-array root) yields an empty list so a command\n// with no arguments (or a hand-broken schema) renders as a bare Send button, never throws.\n// Each descriptor is NORMALIZED (enum\u2192string[], default\u2192string, bounds\u2192finite number,\n// kind/dataType\u2192known value) so a hand-edited dashboard definition with wrong-typed fields\n// (e.g. `enum: \"on\"` or `default: 5`) can't crash the form at render/send time.\nexport function parseParameterSchema(schema: string | null | undefined): CommandParameter[] {\n if (!schema) return [];\n let parsed: unknown;\n try {\n parsed = JSON.parse(schema);\n } catch {\n return [];\n }\n if (!Array.isArray(parsed)) return [];\n const out: CommandParameter[] = [];\n for (const raw of parsed) {\n const norm = normalizeParam(raw);\n if (norm) out.push(norm);\n }\n return out;\n}\n\n// normalizeParam validates one descriptor and coerces every field to its expected type,\n// dropping the descriptor entirely if it has no string `name`. Unknown/wrong-typed fields\n// are discarded rather than trusted, so downstream code never sees a non-array enum or a\n// numeric default.\nfunction normalizeParam(raw: unknown): CommandParameter | null {\n if (typeof raw !== 'object' || raw === null) return null;\n const r = raw as Record<string, unknown>;\n if (typeof r.name !== 'string') return null;\n\n const param: CommandParameter = { name: r.name };\n if (typeof r.description === 'string') param.description = r.description;\n if (r.kind === 'OBJECT') param.kind = 'OBJECT';\n else if (r.kind === 'SCALAR') param.kind = 'SCALAR';\n if (typeof r.dataType === 'string' && DATA_TYPES.has(r.dataType)) {\n param.dataType = r.dataType as CommandParameter['dataType'];\n }\n if (typeof r.unit === 'string') param.unit = r.unit;\n if (r.required === true) param.required = true;\n if (typeof r.default === 'string') param.default = r.default;\n if (typeof r.minValue === 'number' && Number.isFinite(r.minValue)) param.minValue = r.minValue;\n if (typeof r.maxValue === 'number' && Number.isFinite(r.maxValue)) param.maxValue = r.maxValue;\n if (Array.isArray(r.enum)) {\n const values = r.enum.filter((v): v is string => typeof v === 'string');\n if (values.length > 0) param.enum = values;\n }\n return param;\n}\n\n// isScalar reports whether a parameter is a typed single value the form renders an input\n// for. Kind absent means SCALAR (the Go default); only an explicit OBJECT is nested.\nexport function isScalar(param: CommandParameter): boolean {\n return param.kind !== 'OBJECT';\n}\n\n// parseBool interprets a schema/string boolean the way the server's strconv.ParseBool\n// does (accepting '1'/'t'/'true'/'yes'/'on', case-insensitively) so a definition default\n// of \"True\" doesn't silently render \u2014 and send \u2014 as false.\nexport function parseBool(raw: string): boolean {\n const s = raw.trim().toLowerCase();\n return s === 'true' || s === '1' || s === 't' || s === 'yes' || s === 'on';\n}\n\n// defaultValues seeds the form's value map from each scalar parameter. A BOOLEAN always\n// gets a concrete 'true'/'false' (default false) \u2014 a checkbox is never \"empty\", so the\n// value must reflect the box's state exactly. Other scalars seed from their declared\n// default (as a string \u2014 inputs are string-backed), else start empty.\nexport function defaultValues(params: CommandParameter[]): Record<string, string> {\n const values: Record<string, string> = {};\n for (const p of params) {\n if (!isScalar(p)) continue;\n if (p.dataType === 'BOOLEAN') values[p.name] = parseBool(p.default ?? '') ? 'true' : 'false';\n else if (p.default != null) values[p.name] = p.default;\n }\n return values;\n}\n\n// validateParams returns a per-parameter error map (empty = valid). It enforces required\n// (a missing value on a required scalar; a BOOLEAN always has a value so is never\n// \"required-missing\"), numeric validity/bounds for INT/DOUBLE, and blocks a REQUIRED\n// OBJECT parameter (it can't be supplied through the typed form).\nexport function validateParams(\n params: CommandParameter[],\n values: Record<string, string>,\n): Record<string, string> {\n const errors: Record<string, string> = {};\n for (const p of params) {\n if (!isScalar(p)) {\n // A required structured parameter can't be satisfied here \u2014 refuse to send an\n // incomplete command rather than silently omitting it.\n if (p.required) errors[p.name] = 'Structured parameter \u2014 not supported here';\n continue;\n }\n if (p.dataType === 'BOOLEAN') continue; // a checkbox always carries a valid value\n const raw = (values[p.name] ?? '').trim();\n if (raw === '') {\n if (p.required) errors[p.name] = 'Required';\n continue; // an empty optional value is fine \u2014 it's just omitted from the payload\n }\n if (p.dataType === 'INT' || p.dataType === 'DOUBLE') {\n const n = Number(raw);\n if (!Number.isFinite(n)) {\n errors[p.name] = 'Must be a number';\n } else if (p.dataType === 'INT' && !Number.isInteger(n)) {\n errors[p.name] = 'Must be a whole number';\n } else if (p.minValue != null && n < p.minValue) {\n errors[p.name] = `Must be \u2265 ${p.minValue}`;\n } else if (p.maxValue != null && n > p.maxValue) {\n errors[p.name] = `Must be \u2264 ${p.maxValue}`;\n }\n }\n }\n return errors;\n}\n\n// buildPayload serializes the typed values to the JSON payload string command-delivery\n// sends the device \u2014 coercing each scalar to its declared type (INT/DOUBLE \u2192 number,\n// BOOLEAN \u2192 boolean, STRING \u2192 string). A BOOLEAN is always included (its checkbox state\n// is meaningful); other empty optional values are omitted. OBJECT parameters are omitted\n// (validateParams blocks a required one first). Returns undefined when no parameter\n// contributes a value (a parameterless command sends no payload).\nexport function buildPayload(\n params: CommandParameter[],\n values: Record<string, string>,\n): string | undefined {\n const payload: Record<string, unknown> = {};\n for (const p of params) {\n if (!isScalar(p)) continue;\n const raw = (values[p.name] ?? '').trim();\n if (p.dataType === 'BOOLEAN') {\n payload[p.name] = parseBool(raw);\n continue;\n }\n if (raw === '') continue;\n payload[p.name] = coerce(p, raw);\n }\n return Object.keys(payload).length === 0 ? undefined : JSON.stringify(payload);\n}\n\nfunction coerce(param: CommandParameter, raw: string): unknown {\n switch (param.dataType) {\n case 'INT':\n case 'DOUBLE': {\n const n = Number(raw);\n return Number.isFinite(n) ? n : raw; // validation gates this; fall back to raw defensively\n }\n case 'BOOLEAN':\n return parseBool(raw);\n default:\n return raw; // STRING (and untyped)\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,35 @@
1
+ // src/value-scale.ts
2
+ import { thresholdBands } from "@devicechain/widget-sdk";
3
+ function readRange(configuredMin, configuredMax, fallback) {
4
+ const min = configuredMin ?? fallback?.min;
5
+ const max = configuredMax ?? fallback?.max;
6
+ if (min === void 0 || max === void 0) return { ok: false, reason: "missing" };
7
+ if (!(max > min)) return { ok: false, reason: "invalid" };
8
+ return { ok: true, range: { min, max } };
9
+ }
10
+ function fillOf(value, range) {
11
+ if (value == null || !Number.isFinite(value)) return void 0;
12
+ const ratio = (value - range.min) / (range.max - range.min);
13
+ const position = value < range.min ? "below" : value > range.max ? "above" : "within";
14
+ return { fraction: Math.min(1, Math.max(0, ratio)), position, ratio };
15
+ }
16
+ function bandSpans(thresholds, range) {
17
+ const width = range.max - range.min;
18
+ return thresholdBands(thresholds, range.min, range.max).map((b) => ({
19
+ from: (b.from - range.min) / width,
20
+ to: (b.to - range.min) / width,
21
+ level: b.level
22
+ }));
23
+ }
24
+ function litBars(thresholds, value) {
25
+ if (value == null || !Number.isFinite(value)) return void 0;
26
+ return { total: thresholds.length, lit: thresholds.filter((t) => value >= t.value).length };
27
+ }
28
+
29
+ export {
30
+ readRange,
31
+ fillOf,
32
+ bandSpans,
33
+ litBars
34
+ };
35
+ //# sourceMappingURL=chunk-ZR74ORIT.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/value-scale.ts"],
4
+ "sourcesContent": ["// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// The arithmetic the level-and-indicator widgets share: reading a [min, max] scale off the\n// options, turning a value into a fill fraction, and laying threshold bands over the scale.\n// Pure and React-free, so every rule is asserted by value in value-scale.test.ts.\n//\n// \uD83D\uDD11 No range is ever invented. A widget that needs a scale either gets BOTH ends from its\n// options or from a fallback its descriptor documents (the battery's 0..100 percent); with\n// neither it reports why, and the widget renders that reason instead of a plausible fill.\n\nimport { thresholdBands, type SeverityLevel, type Threshold } from '@devicechain/widget-sdk';\n\nexport interface ScaleRange {\n min: number;\n max: number;\n}\n\nexport type RangeResult = { ok: true; range: ScaleRange } | { ok: false; reason: 'missing' | 'invalid' };\n\n// The scale from the two configured ends. With a `fallback`, an end left unset (undefined) takes\n// the fallback's (the documented default); without one, an unset end is `missing`. An inverted\n// or empty scale (max <= min) is `invalid`: a descriptor constraint says the same at authoring\n// time, but a hand-edited definition reaches the renderer without it.\nexport function readRange(configuredMin: number | undefined, configuredMax: number | undefined, fallback?: ScaleRange): RangeResult {\n const min = configuredMin ?? fallback?.min;\n const max = configuredMax ?? fallback?.max;\n if (min === undefined || max === undefined) return { ok: false, reason: 'missing' };\n if (!(max > min)) return { ok: false, reason: 'invalid' };\n return { ok: true, range: { min, max } };\n}\n\nexport type ScalePosition = 'below' | 'within' | 'above';\n\nexport interface Fill {\n // 0..1, clamped: what is drawn.\n fraction: number;\n // Where the value really is. Anything but `within` is marked on the widget, so a clamped fill\n // is never mistaken for a value that sits exactly on the end.\n position: ScalePosition;\n // The unclamped (value - min) / (max - min), for a percent readout.\n ratio: number;\n}\n\n// The fill for a value on a scale, or undefined with no usable value (null, NaN, +-Infinity).\nexport function fillOf(value: number | null | undefined, range: ScaleRange): Fill | undefined {\n if (value == null || !Number.isFinite(value)) return undefined;\n const ratio = (value - range.min) / (range.max - range.min);\n const position: ScalePosition = value < range.min ? 'below' : value > range.max ? 'above' : 'within';\n return { fraction: Math.min(1, Math.max(0, ratio)), position, ratio };\n}\n\nexport interface BandSpan {\n // Fractions of the scale, 0..1.\n from: number;\n to: number;\n level: SeverityLevel | undefined;\n}\n\n// The threshold bands as fractions of the scale: what a track draws behind the fill.\nexport function bandSpans(thresholds: readonly Threshold[], range: ScaleRange): BandSpan[] {\n const width = range.max - range.min;\n return thresholdBands(thresholds, range.min, range.max).map((b) => ({\n from: (b.from - range.min) / width,\n to: (b.to - range.min) / width,\n level: b.level,\n }));\n}\n\n// A signal strength as bars: one bar per threshold, lit for every threshold the value has\n// reached. Below the lowest threshold none is lit, which is a truthful \"no signal\", not a gap.\nexport function litBars(thresholds: readonly Threshold[], value: number | null | undefined): { total: number; lit: number } | undefined {\n if (value == null || !Number.isFinite(value)) return undefined;\n return { total: thresholds.length, lit: thresholds.filter((t) => value >= t.value).length };\n}\n"],
5
+ "mappings": ";AAWA,SAAS,sBAA0D;AAa5D,SAAS,UAAU,eAAmC,eAAmC,UAAoC;AAClI,QAAM,MAAM,iBAAiB,UAAU;AACvC,QAAM,MAAM,iBAAiB,UAAU;AACvC,MAAI,QAAQ,UAAa,QAAQ,OAAW,QAAO,EAAE,IAAI,OAAO,QAAQ,UAAU;AAClF,MAAI,EAAE,MAAM,KAAM,QAAO,EAAE,IAAI,OAAO,QAAQ,UAAU;AACxD,SAAO,EAAE,IAAI,MAAM,OAAO,EAAE,KAAK,IAAI,EAAE;AACzC;AAeO,SAAS,OAAO,OAAkC,OAAqC;AAC5F,MAAI,SAAS,QAAQ,CAAC,OAAO,SAAS,KAAK,EAAG,QAAO;AACrD,QAAM,SAAS,QAAQ,MAAM,QAAQ,MAAM,MAAM,MAAM;AACvD,QAAM,WAA0B,QAAQ,MAAM,MAAM,UAAU,QAAQ,MAAM,MAAM,UAAU;AAC5F,SAAO,EAAE,UAAU,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,KAAK,CAAC,GAAG,UAAU,MAAM;AACtE;AAUO,SAAS,UAAU,YAAkC,OAA+B;AACzF,QAAM,QAAQ,MAAM,MAAM,MAAM;AAChC,SAAO,eAAe,YAAY,MAAM,KAAK,MAAM,GAAG,EAAE,IAAI,CAAC,OAAO;AAAA,IAClE,OAAO,EAAE,OAAO,MAAM,OAAO;AAAA,IAC7B,KAAK,EAAE,KAAK,MAAM,OAAO;AAAA,IACzB,OAAO,EAAE;AAAA,EACX,EAAE;AACJ;AAIO,SAAS,QAAQ,YAAkC,OAA8E;AACtI,MAAI,SAAS,QAAQ,CAAC,OAAO,SAAS,KAAK,EAAG,QAAO;AACrD,SAAO,EAAE,OAAO,WAAW,QAAQ,KAAK,WAAW,OAAO,CAAC,MAAM,SAAS,EAAE,KAAK,EAAE,OAAO;AAC5F;",
6
+ "names": []
7
+ }