@devicechain/widgets 0.22.0 → 0.23.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.
- package/README.md +15 -0
- package/dist/access-strings.d.ts +6 -0
- package/dist/chunks/{aggregate-feed-6ZYKOQOB.js → aggregate-feed-BD3CHNFU.js} +5 -4
- package/dist/chunks/aggregate-feed-BD3CHNFU.js.map +7 -0
- package/dist/chunks/{aggregate-widgets-WVXAW3UL.js → aggregate-widgets-HZQI5HTX.js} +3 -2
- package/dist/chunks/{aggregate-widgets-WVXAW3UL.js.map → aggregate-widgets-HZQI5HTX.js.map} +1 -1
- package/dist/chunks/alarm-widgets-7UY6MIYZ.js +9 -0
- package/dist/chunks/alarm-widgets-7UY6MIYZ.js.map +7 -0
- package/dist/chunks/chunk-6OGI4UFJ.js +367 -0
- package/dist/chunks/chunk-6OGI4UFJ.js.map +7 -0
- package/dist/chunks/chunk-BRH66T5F.js +15 -0
- package/dist/chunks/chunk-BRH66T5F.js.map +7 -0
- package/dist/chunks/chunk-FM7OI5TQ.js +87 -0
- package/dist/chunks/chunk-FM7OI5TQ.js.map +7 -0
- package/dist/chunks/chunk-USRYKW2J.js +55 -0
- package/dist/chunks/chunk-USRYKW2J.js.map +7 -0
- package/dist/chunks/{chunk-S2OMBJLI.js → chunk-WBKHYZU2.js} +7 -3
- package/dist/chunks/{chunk-S2OMBJLI.js.map → chunk-WBKHYZU2.js.map} +2 -2
- package/dist/chunks/chunk-XSLFHCNI.js +419 -0
- package/dist/chunks/chunk-XSLFHCNI.js.map +7 -0
- package/dist/chunks/{control-widgets-UDS4ET3T.js → control-widgets-3V2LSSRF.js} +4 -2
- package/dist/chunks/{control-widgets-UDS4ET3T.js.map → control-widgets-3V2LSSRF.js.map} +1 -1
- package/dist/chunks/entity-selector-3JJFI4DT.js +7 -0
- package/dist/chunks/entity-selector-3JJFI4DT.js.map +7 -0
- package/dist/chunks/{fleet-feed-FK4ZMKIK.js → fleet-feed-5QGDLOV5.js} +27 -7
- package/dist/chunks/fleet-feed-5QGDLOV5.js.map +7 -0
- package/dist/chunks/table-6EVV7QZH.js +7 -0
- package/dist/chunks/table-6EVV7QZH.js.map +7 -0
- package/dist/connected-widget.d.ts +2 -0
- package/dist/dashboard-renderer.d.ts +1 -0
- package/dist/fleet-feed.d.ts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +478 -1142
- package/dist/index.js.map +4 -4
- package/dist/preload.d.ts +2 -0
- package/dist/widgets/alarm-count.d.ts +0 -46
- package/dist/widgets/alarm-lazy.d.ts +123 -0
- package/dist/widgets/alarm-table.d.ts +0 -76
- package/dist/widgets/alarm-widgets.d.ts +2 -0
- package/dist/widgets/command-button.d.ts +0 -38
- package/dist/widgets/control-lazy.d.ts +38 -0
- package/dist/widgets/control-widgets.d.ts +1 -0
- package/dist/widgets/entity-selector.d.ts +0 -15
- package/dist/widgets/gauge.d.ts +4 -0
- package/dist/widgets/lazy-chunk.d.ts +2 -1
- package/dist/widgets/selection-lazy.d.ts +16 -0
- package/dist/widgets/table-lazy.d.ts +39 -0
- package/dist/widgets/table.d.ts +0 -38
- package/dist/widgets/time-series-chart.d.ts +4 -0
- package/package.json +10 -7
- package/dist/chunks/aggregate-feed-6ZYKOQOB.js.map +0 -7
- package/dist/chunks/chunk-YPFB6DAL.js +0 -157
- package/dist/chunks/chunk-YPFB6DAL.js.map +0 -7
- package/dist/chunks/fleet-feed-FK4ZMKIK.js.map +0 -7
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
// src/widgets/severity.ts
|
|
2
|
+
import { css } from "@devicechain/widget-sdk";
|
|
3
|
+
var LEVEL_OF = {
|
|
4
|
+
CRITICAL: "critical",
|
|
5
|
+
MAJOR: "major",
|
|
6
|
+
MINOR: "minor",
|
|
7
|
+
WARNING: "warning"
|
|
8
|
+
};
|
|
9
|
+
var SEVERITY_ORDER = {
|
|
10
|
+
CRITICAL: 0,
|
|
11
|
+
MAJOR: 1,
|
|
12
|
+
MINOR: 2,
|
|
13
|
+
WARNING: 3,
|
|
14
|
+
INDETERMINATE: 4
|
|
15
|
+
};
|
|
16
|
+
function severityColor(colors, severity) {
|
|
17
|
+
const level = LEVEL_OF[severity];
|
|
18
|
+
return level ? colors[level] : css("muted-foreground");
|
|
19
|
+
}
|
|
20
|
+
function severityRank(severity) {
|
|
21
|
+
return SEVERITY_ORDER[severity] ?? 99;
|
|
22
|
+
}
|
|
23
|
+
function severityLabel(severity) {
|
|
24
|
+
if (!severity) return "";
|
|
25
|
+
return severity.charAt(0).toUpperCase() + severity.slice(1).toLowerCase();
|
|
26
|
+
}
|
|
27
|
+
function highestSeverity(severities) {
|
|
28
|
+
let best;
|
|
29
|
+
for (const s of severities) {
|
|
30
|
+
if (best === void 0 || severityRank(s) < severityRank(best)) best = s;
|
|
31
|
+
}
|
|
32
|
+
return best;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// src/widgets/alarm-table.tsx
|
|
36
|
+
import { Fragment, useState } from "react";
|
|
37
|
+
import {
|
|
38
|
+
css as css2,
|
|
39
|
+
fontVar,
|
|
40
|
+
FlashValue,
|
|
41
|
+
formatDateTime,
|
|
42
|
+
optBoolean,
|
|
43
|
+
optNumber,
|
|
44
|
+
optString,
|
|
45
|
+
useElementSize,
|
|
46
|
+
useSeverityColors,
|
|
47
|
+
useWidgetSelect,
|
|
48
|
+
useWidgetTimeZone,
|
|
49
|
+
WidgetFrame
|
|
50
|
+
} from "@devicechain/widget-sdk";
|
|
51
|
+
import { Fragment as Fragment2, jsx, jsxs } from "react/jsx-runtime";
|
|
52
|
+
var cell = { padding: "6px 10px", textAlign: "left", whiteSpace: "nowrap" };
|
|
53
|
+
var headCell = {
|
|
54
|
+
...cell,
|
|
55
|
+
position: "sticky",
|
|
56
|
+
top: 0,
|
|
57
|
+
background: css2("card"),
|
|
58
|
+
fontSize: fontVar("xs"),
|
|
59
|
+
fontWeight: 600,
|
|
60
|
+
textTransform: "uppercase",
|
|
61
|
+
letterSpacing: "0.04em",
|
|
62
|
+
color: css2("muted-foreground"),
|
|
63
|
+
borderBottom: `1px solid ${css2("border")}`
|
|
64
|
+
};
|
|
65
|
+
var ALARM_TABLE_NARROW_PX = 480;
|
|
66
|
+
var narrowCell = {
|
|
67
|
+
...cell,
|
|
68
|
+
padding: "6px 6px",
|
|
69
|
+
whiteSpace: "normal",
|
|
70
|
+
overflowWrap: "break-word",
|
|
71
|
+
wordBreak: "normal"
|
|
72
|
+
};
|
|
73
|
+
function hyphenBreaks(text) {
|
|
74
|
+
const parts = text.split("-").map((p, i, all) => i < all.length - 1 ? `${p}-` : p);
|
|
75
|
+
return parts.map((part, i) => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
76
|
+
i > 0 ? /* @__PURE__ */ jsx("wbr", {}) : null,
|
|
77
|
+
part
|
|
78
|
+
] }, i));
|
|
79
|
+
}
|
|
80
|
+
function statusLabel(alarm) {
|
|
81
|
+
if (alarm.state === "CLEARED") return "Cleared";
|
|
82
|
+
return alarm.acknowledged ? "Acknowledged" : "Active";
|
|
83
|
+
}
|
|
84
|
+
function AlarmTable({ widget, data, actions }) {
|
|
85
|
+
const { alarms, total, loading } = data;
|
|
86
|
+
const canAct = actions?.can("alarm:write") ?? false;
|
|
87
|
+
const [sizeRef, size] = useElementSize();
|
|
88
|
+
const severity = useSeverityColors();
|
|
89
|
+
const narrow = size.width > 0 && size.width < ALARM_TABLE_NARROW_PX;
|
|
90
|
+
const columns = 5 + (narrow ? 0 : 2) + (canAct ? 1 : 0);
|
|
91
|
+
const text = narrow ? narrowCell : cell;
|
|
92
|
+
const precision = optNumber(widget.options, "precision");
|
|
93
|
+
const flash = optBoolean(widget.options, "flashOnChange");
|
|
94
|
+
const select = useWidgetSelect();
|
|
95
|
+
const timeZone = useWidgetTimeZone();
|
|
96
|
+
const selectionTarget = optString(widget.options, "selectionTarget");
|
|
97
|
+
const drillTo = select && selectionTarget ? { select, slot: selectionTarget } : void 0;
|
|
98
|
+
return /* @__PURE__ */ jsx(WidgetFrame, { title: optString(widget.options, "title"), flush: true, children: /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", height: "100%" }, children: [
|
|
99
|
+
/* @__PURE__ */ jsx("div", { ref: sizeRef, style: { flex: 1, minHeight: 0, overflow: "auto" }, children: /* @__PURE__ */ jsxs(
|
|
100
|
+
"table",
|
|
101
|
+
{
|
|
102
|
+
style: {
|
|
103
|
+
width: "100%",
|
|
104
|
+
borderCollapse: "collapse",
|
|
105
|
+
fontSize: fontVar("md"),
|
|
106
|
+
color: css2("foreground"),
|
|
107
|
+
fontVariantNumeric: "tabular-nums"
|
|
108
|
+
},
|
|
109
|
+
children: [
|
|
110
|
+
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
111
|
+
/* @__PURE__ */ jsx("th", { style: { ...headCell, width: 4, padding: 0 }, "aria-hidden": "true" }),
|
|
112
|
+
/* @__PURE__ */ jsx("th", { scope: "col", style: headCell, children: "Severity" }),
|
|
113
|
+
narrow ? null : /* @__PURE__ */ jsx("th", { scope: "col", style: headCell, children: "Status" }),
|
|
114
|
+
/* @__PURE__ */ jsx("th", { scope: "col", style: headCell, children: "Originator" }),
|
|
115
|
+
/* @__PURE__ */ jsx("th", { scope: "col", style: headCell, children: "Alarm" }),
|
|
116
|
+
/* @__PURE__ */ jsx("th", { scope: "col", style: { ...headCell, textAlign: "right" }, children: "Value" }),
|
|
117
|
+
narrow ? null : /* @__PURE__ */ jsx("th", { scope: "col", style: headCell, children: "Raised" }),
|
|
118
|
+
canAct ? /* @__PURE__ */ jsx("th", { scope: "col", style: { ...headCell, textAlign: "right" }, children: "Actions" }) : null
|
|
119
|
+
] }) }),
|
|
120
|
+
/* @__PURE__ */ jsx("tbody", { children: alarms.length === 0 ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx("td", { colSpan: columns, style: { ...cell, textAlign: "center", color: css2("muted-foreground") }, children: loading ? "Loading\u2026" : "No alarms" }) }) : alarms.map((alarm) => {
|
|
121
|
+
const color = severityColor(severity, alarm.severity);
|
|
122
|
+
return /* @__PURE__ */ jsxs("tr", { style: { borderBottom: `1px solid ${css2("border")}` }, children: [
|
|
123
|
+
/* @__PURE__ */ jsx("td", { "aria-hidden": "true", style: { padding: 0, width: 4, background: color } }),
|
|
124
|
+
/* @__PURE__ */ jsx("td", { style: cell, children: /* @__PURE__ */ jsxs("span", { style: { display: "inline-flex", alignItems: "center", gap: 6 }, children: [
|
|
125
|
+
/* @__PURE__ */ jsx(
|
|
126
|
+
"span",
|
|
127
|
+
{
|
|
128
|
+
"aria-hidden": "true",
|
|
129
|
+
style: { width: 8, height: 8, borderRadius: "50%", background: color, flexShrink: 0 }
|
|
130
|
+
}
|
|
131
|
+
),
|
|
132
|
+
severityLabel(alarm.severity)
|
|
133
|
+
] }) }),
|
|
134
|
+
narrow ? null : /* @__PURE__ */ jsx("td", { style: { ...cell, color: css2("muted-foreground") }, children: statusLabel(alarm) }),
|
|
135
|
+
/* @__PURE__ */ jsx("td", { style: text, children: /* @__PURE__ */ jsx(OriginatorCell, { alarm, drillTo }) }),
|
|
136
|
+
/* @__PURE__ */ jsx("td", { style: text, children: hyphenBreaks(alarm.alarmKey) }),
|
|
137
|
+
/* @__PURE__ */ jsx("td", { style: { ...cell, textAlign: "right" }, children: /* @__PURE__ */ jsx(
|
|
138
|
+
FlashValue,
|
|
139
|
+
{
|
|
140
|
+
value: alarm.lastValue,
|
|
141
|
+
precision,
|
|
142
|
+
enabled: flash,
|
|
143
|
+
identity: alarm.token
|
|
144
|
+
}
|
|
145
|
+
) }),
|
|
146
|
+
narrow ? null : /* @__PURE__ */ jsx("td", { style: { ...cell, color: css2("muted-foreground") }, children: formatDateTime(alarm.raisedTime, timeZone) }),
|
|
147
|
+
canAct ? /* @__PURE__ */ jsx("td", { style: { ...cell, textAlign: "right" }, children: /* @__PURE__ */ jsx(AlarmRowActions, { alarm, actions }) }) : null
|
|
148
|
+
] }, alarm.token);
|
|
149
|
+
}) })
|
|
150
|
+
]
|
|
151
|
+
}
|
|
152
|
+
) }),
|
|
153
|
+
total > alarms.length ? /* @__PURE__ */ jsxs(
|
|
154
|
+
"div",
|
|
155
|
+
{
|
|
156
|
+
style: {
|
|
157
|
+
padding: "4px 10px",
|
|
158
|
+
fontSize: fontVar("xs"),
|
|
159
|
+
color: css2("muted-foreground"),
|
|
160
|
+
borderTop: `1px solid ${css2("border")}`
|
|
161
|
+
},
|
|
162
|
+
children: [
|
|
163
|
+
"Showing ",
|
|
164
|
+
alarms.length,
|
|
165
|
+
" of ",
|
|
166
|
+
total
|
|
167
|
+
]
|
|
168
|
+
}
|
|
169
|
+
) : null
|
|
170
|
+
] }) });
|
|
171
|
+
}
|
|
172
|
+
function OriginatorCell({
|
|
173
|
+
alarm,
|
|
174
|
+
drillTo
|
|
175
|
+
}) {
|
|
176
|
+
if (drillTo && alarm.originatorType === "device" && alarm.originatorToken) {
|
|
177
|
+
const token = alarm.originatorToken;
|
|
178
|
+
return /* @__PURE__ */ jsx(
|
|
179
|
+
"button",
|
|
180
|
+
{
|
|
181
|
+
type: "button",
|
|
182
|
+
onClick: () => drillTo.select({ slot: drillTo.slot, binding: { kind: "device", deviceToken: token } }),
|
|
183
|
+
title: `Show ${token} in the linked widgets`,
|
|
184
|
+
style: {
|
|
185
|
+
padding: 0,
|
|
186
|
+
border: "none",
|
|
187
|
+
background: "none",
|
|
188
|
+
font: "inherit",
|
|
189
|
+
color: css2("primary"),
|
|
190
|
+
cursor: "pointer",
|
|
191
|
+
textDecoration: "underline",
|
|
192
|
+
textUnderlineOffset: 2
|
|
193
|
+
},
|
|
194
|
+
children: hyphenBreaks(token)
|
|
195
|
+
}
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
return /* @__PURE__ */ jsx(Fragment2, { children: hyphenBreaks(alarm.originatorToken ?? alarm.originatorType) });
|
|
199
|
+
}
|
|
200
|
+
var actionButton = {
|
|
201
|
+
padding: "2px 8px",
|
|
202
|
+
fontSize: fontVar("sm"),
|
|
203
|
+
lineHeight: 1.4,
|
|
204
|
+
borderRadius: 4,
|
|
205
|
+
border: `1px solid ${css2("border")}`,
|
|
206
|
+
background: css2("card"),
|
|
207
|
+
color: css2("foreground"),
|
|
208
|
+
cursor: "pointer"
|
|
209
|
+
};
|
|
210
|
+
function AlarmRowActions({ alarm, actions }) {
|
|
211
|
+
const severity = useSeverityColors();
|
|
212
|
+
const [pending, setPending] = useState(null);
|
|
213
|
+
const [failed, setFailed] = useState(false);
|
|
214
|
+
if (alarm.state !== "ACTIVE") {
|
|
215
|
+
return /* @__PURE__ */ jsx("span", { style: { color: css2("muted-foreground") }, children: "\u2014" });
|
|
216
|
+
}
|
|
217
|
+
const run = (kind, op) => {
|
|
218
|
+
setPending(kind);
|
|
219
|
+
setFailed(false);
|
|
220
|
+
let promise;
|
|
221
|
+
try {
|
|
222
|
+
promise = op();
|
|
223
|
+
} catch {
|
|
224
|
+
setFailed(true);
|
|
225
|
+
setPending(null);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
promise.catch(() => setFailed(true)).finally(() => setPending(null));
|
|
229
|
+
};
|
|
230
|
+
return /* @__PURE__ */ jsxs("span", { style: { display: "inline-flex", gap: 6, justifyContent: "flex-end", alignItems: "center" }, children: [
|
|
231
|
+
failed ? /* @__PURE__ */ jsx(
|
|
232
|
+
"span",
|
|
233
|
+
{
|
|
234
|
+
role: "img",
|
|
235
|
+
"aria-label": "Action failed \u2014 retry",
|
|
236
|
+
title: "Action failed \u2014 retry",
|
|
237
|
+
style: { color: severityColor(severity, "CRITICAL"), fontSize: fontVar("sm") },
|
|
238
|
+
children: "\u26A0"
|
|
239
|
+
}
|
|
240
|
+
) : null,
|
|
241
|
+
!alarm.acknowledged ? /* @__PURE__ */ jsx(
|
|
242
|
+
"button",
|
|
243
|
+
{
|
|
244
|
+
type: "button",
|
|
245
|
+
style: { ...actionButton, opacity: pending ? 0.6 : 1 },
|
|
246
|
+
disabled: pending !== null,
|
|
247
|
+
"aria-busy": pending === "ack",
|
|
248
|
+
"aria-label": `Acknowledge alarm ${alarm.alarmKey}`,
|
|
249
|
+
onClick: () => run("ack", () => actions.acknowledgeAlarm(alarm.token)),
|
|
250
|
+
children: pending === "ack" ? "\u2026" : "Ack"
|
|
251
|
+
}
|
|
252
|
+
) : null,
|
|
253
|
+
/* @__PURE__ */ jsx(
|
|
254
|
+
"button",
|
|
255
|
+
{
|
|
256
|
+
type: "button",
|
|
257
|
+
style: { ...actionButton, opacity: pending ? 0.6 : 1 },
|
|
258
|
+
disabled: pending !== null,
|
|
259
|
+
"aria-busy": pending === "clear",
|
|
260
|
+
"aria-label": `Clear alarm ${alarm.alarmKey}`,
|
|
261
|
+
onClick: () => run("clear", () => actions.clearAlarm(alarm.token)),
|
|
262
|
+
children: pending === "clear" ? "\u2026" : "Clear"
|
|
263
|
+
}
|
|
264
|
+
)
|
|
265
|
+
] });
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// src/widgets/alarm-count.tsx
|
|
269
|
+
import {
|
|
270
|
+
css as css3,
|
|
271
|
+
fitValueFont,
|
|
272
|
+
fitValueFontCss,
|
|
273
|
+
FIT_MIN_FONT_PX,
|
|
274
|
+
fontVar as fontVar2,
|
|
275
|
+
optString as optString2,
|
|
276
|
+
spaceVar,
|
|
277
|
+
useSeverityColors as useSeverityColors2,
|
|
278
|
+
useStickyChars,
|
|
279
|
+
useWidgetScale,
|
|
280
|
+
WidgetFrame as WidgetFrame2
|
|
281
|
+
} from "@devicechain/widget-sdk";
|
|
282
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
283
|
+
var ALARM_COUNT_CAPTION_ROOM_PX = 20;
|
|
284
|
+
function AlarmCountBody({ data }) {
|
|
285
|
+
const { size, scale } = useWidgetScale();
|
|
286
|
+
const severity = useSeverityColors2();
|
|
287
|
+
const { total, alarms, loading } = data;
|
|
288
|
+
const worst = highestSeverity(alarms.map((a) => a.severity));
|
|
289
|
+
const alerting = total > 0 && worst != null;
|
|
290
|
+
const color = alerting ? severityColor(severity, worst) : css3("muted-foreground");
|
|
291
|
+
const display = loading && total === 0 ? "\u2014" : String(total);
|
|
292
|
+
const chars = useStickyChars(display.length);
|
|
293
|
+
const floor = Math.round(FIT_MIN_FONT_PX * scale);
|
|
294
|
+
const measured = size.width > 0 && size.height > 0;
|
|
295
|
+
const valueFont = measured ? fitValueFont(
|
|
296
|
+
{ width: size.width, height: Math.max(1, size.height - ALARM_COUNT_CAPTION_ROOM_PX * scale) },
|
|
297
|
+
chars,
|
|
298
|
+
{ floor }
|
|
299
|
+
) : (
|
|
300
|
+
// First frame: the same curve in cq units, resolved by the browser against the frame's
|
|
301
|
+
// column (it does not take the caption's room off; the first measure does).
|
|
302
|
+
fitValueFontCss(chars, { floor })
|
|
303
|
+
);
|
|
304
|
+
const unit = total === 1 ? "alarm" : "alarms";
|
|
305
|
+
const ariaLabel = alerting ? `${total} ${unit}, worst severity ${severityLabel(worst)}` : `${total} ${unit}`;
|
|
306
|
+
return /* @__PURE__ */ jsxs2(
|
|
307
|
+
"div",
|
|
308
|
+
{
|
|
309
|
+
style: {
|
|
310
|
+
display: "flex",
|
|
311
|
+
flexDirection: "column",
|
|
312
|
+
justifyContent: "safe center",
|
|
313
|
+
alignItems: "center",
|
|
314
|
+
height: "100%",
|
|
315
|
+
boxSizing: "border-box",
|
|
316
|
+
overflow: "hidden",
|
|
317
|
+
textAlign: "center"
|
|
318
|
+
},
|
|
319
|
+
children: [
|
|
320
|
+
/* @__PURE__ */ jsx2(
|
|
321
|
+
"div",
|
|
322
|
+
{
|
|
323
|
+
"aria-label": ariaLabel,
|
|
324
|
+
style: {
|
|
325
|
+
fontSize: valueFont,
|
|
326
|
+
fontWeight: 700,
|
|
327
|
+
lineHeight: 1,
|
|
328
|
+
whiteSpace: "nowrap",
|
|
329
|
+
fontVariantNumeric: "tabular-nums",
|
|
330
|
+
color
|
|
331
|
+
},
|
|
332
|
+
children: display
|
|
333
|
+
}
|
|
334
|
+
),
|
|
335
|
+
/* @__PURE__ */ jsxs2(
|
|
336
|
+
"div",
|
|
337
|
+
{
|
|
338
|
+
style: {
|
|
339
|
+
fontSize: fontVar2("xs"),
|
|
340
|
+
color: css3("muted-foreground"),
|
|
341
|
+
marginTop: spaceVar(1),
|
|
342
|
+
textTransform: "uppercase",
|
|
343
|
+
letterSpacing: "0.05em"
|
|
344
|
+
},
|
|
345
|
+
children: [
|
|
346
|
+
/* @__PURE__ */ jsx2("span", { children: unit }),
|
|
347
|
+
alerting ? /* @__PURE__ */ jsx2("span", { children: ` \xB7 worst ${severityLabel(worst)}` }) : null
|
|
348
|
+
]
|
|
349
|
+
}
|
|
350
|
+
)
|
|
351
|
+
]
|
|
352
|
+
}
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
function AlarmCount({ widget, data }) {
|
|
356
|
+
return /* @__PURE__ */ jsx2(WidgetFrame2, { title: optString2(widget.options, "title"), children: /* @__PURE__ */ jsx2(AlarmCountBody, { data }) });
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export {
|
|
360
|
+
severityColor,
|
|
361
|
+
severityRank,
|
|
362
|
+
severityLabel,
|
|
363
|
+
highestSeverity,
|
|
364
|
+
AlarmTable,
|
|
365
|
+
AlarmCount
|
|
366
|
+
};
|
|
367
|
+
//# sourceMappingURL=chunk-6OGI4UFJ.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/widgets/severity.ts", "../../src/widgets/alarm-table.tsx", "../../src/widgets/alarm-count.tsx"],
|
|
4
|
+
"sourcesContent": ["// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// Alarm-severity presentation shared by the alarm widgets (ADR-041 vocabulary:\n// CRITICAL | MAJOR | MINOR | WARNING | INDETERMINATE). Severity is SEMANTIC color --\n// deliberately fixed, not derived from the dashboard accent -- so a critical alarm reads the\n// same red on every tenant's theme.\n//\n// \uD83D\uDD11 ONE PALETTE. The hues are the SDK's severity tokens (`--sev-*`, widget-sdk/severity.ts):\n// the same colour a threshold band uses is the colour an alarm row uses, in light and dark, and\n// a host that re-themes a token re-themes both. This file keeps only the VOCABULARY mapping\n// (the backend's UPPER_CASE severities onto the token levels). INDETERMINATE -- and anything\n// unrecognized -- has no alarm hue: it renders in the muted foreground, the same neutral an\n// absent alarm gets.\n\nimport { css, type AlarmLevel } from '@devicechain/widget-sdk';\n\nconst LEVEL_OF: Record<string, AlarmLevel> = {\n CRITICAL: 'critical',\n MAJOR: 'major',\n MINOR: 'minor',\n WARNING: 'warning',\n};\n\nconst SEVERITY_ORDER: Record<string, number> = {\n CRITICAL: 0,\n MAJOR: 1,\n MINOR: 2,\n WARNING: 3,\n INDETERMINATE: 4,\n};\n\n// severityColor maps a severity to its badge/stripe color. `colors` is useSeverityColors() --\n// the concrete token colours in scope -- so the result follows the host's theme and dark mode.\n// An INDETERMINATE or unrecognized severity (a hand-edited or future one) takes the muted\n// foreground rather than a hue it has not earned.\nexport function severityColor(colors: Readonly<Record<AlarmLevel, string>>, severity: string): string {\n const level = LEVEL_OF[severity];\n return level ? colors[level] : css('muted-foreground');\n}\n\n// severityRank orders severities most-severe-first; unknown severities sort last.\nexport function severityRank(severity: string): number {\n return SEVERITY_ORDER[severity] ?? 99;\n}\n\n// severityLabel renders a severity in Title Case ('CRITICAL' \u2192 'Critical').\nexport function severityLabel(severity: string): string {\n if (!severity) return '';\n return severity.charAt(0).toUpperCase() + severity.slice(1).toLowerCase();\n}\n\n// highestSeverity returns the most severe severity present in a list, or undefined for\n// an empty list \u2014 the accent an alarm-count uses to signal \"how bad is the worst one\".\nexport function highestSeverity(severities: string[]): string | undefined {\n let best: string | undefined;\n for (const s of severities) {\n if (best === undefined || severityRank(s) < severityRank(best)) best = s;\n }\n return best;\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// alarm-table \u2014 the raised alarms in the widget's scope (ADR-041), newest first, with\n// a severity stripe per row. When the runtime supplies an action seam AND the viewer\n// holds alarm:write, each active row gets Acknowledge / Clear controls; otherwise the\n// table is read-only. Bound through the hub's alarm channel, so it renders identically\n// from live data or the synthetic preview source.\n\nimport type { AlarmRow, WidgetActions } from '@devicechain/dashboards';\nimport { Fragment, useState, type CSSProperties, type ReactNode } from 'react';\n\nimport {\n type AlarmStreamState,\n css,\n fontVar,\n FlashValue,\n formatDateTime,\n optBoolean,\n optNumber,\n optString,\n useElementSize,\n useSeverityColors,\n useWidgetSelect,\n useWidgetTimeZone,\n WidgetFrame,\n type WidgetProps,\n type WidgetSelect,\n} from '@devicechain/widget-sdk';\n\nimport { severityColor, severityLabel } from './severity';\n\nconst cell: CSSProperties = { padding: '6px 10px', textAlign: 'left', whiteSpace: 'nowrap' };\nconst headCell: CSSProperties = {\n ...cell,\n position: 'sticky',\n top: 0,\n background: css('card'),\n fontSize: fontVar('xs'),\n fontWeight: 600,\n textTransform: 'uppercase',\n letterSpacing: '0.04em',\n color: css('muted-foreground'),\n borderBottom: `1px solid ${css('border')}`,\n};\n\n// statusLabel collapses the 4-state model (state \u00D7 acknowledged) into the one signal an\n// operator reads, matching the console's alarm badges: cleared wins regardless of the\n// acknowledged flag; an active alarm is \"Acknowledged\" once owned, else \"Active\".\n// Below this measured width the table drops its lowest-value columns (Status, Raised) rather\n// than clipping or scrolling sideways: severity, originator, alarm and value are what an\n// operator reads at a glance, and the text columns wrap instead of pushing the value off the\n// right edge (a phone's single column is ~390 px). Unmeasured (first frame, jsdom) keeps every\n// column on one line.\nexport const ALARM_TABLE_NARROW_PX = 480;\n\n// A narrow table's text cells: tighter padding, and a long originator or alarm key wraps\n// across lines rather than widening the table past its box. Identifiers break at their\n// hyphens first (see hyphenBreaks); `break-word` splits inside a word only when a single\n// segment is wider than the cell. `anywhere` is deliberately NOT used: it also lowers the\n// column's min-content width, so a squeezed column was split mid-word (\"engine-overhea|t\")\n// instead of at a hyphen.\nconst narrowCell: CSSProperties = {\n ...cell,\n padding: '6px 6px',\n whiteSpace: 'normal',\n overflowWrap: 'break-word',\n wordBreak: 'normal',\n};\n\n// hyphenBreaks renders an identifier with a break opportunity after each hyphen, so a\n// hyphenated key (\"tyre-pressure-low\") wraps as \"tyre-/pressure-/low\". The <wbr> also keeps\n// the column's min-content width at the longest segment, which is what lets the table\n// shrink at 390 px without pushing the Value column off the edge.\nexport function hyphenBreaks(text: string): ReactNode {\n // Split after each hyphen, keeping it on the left part. Not a lookbehind regex: this is a\n // published package, and a lookbehind literal is a parse error in older WebKit.\n const parts = text.split('-').map((p, i, all) => (i < all.length - 1 ? `${p}-` : p));\n return parts.map((part, i) => (\n <Fragment key={i}>\n {i > 0 ? <wbr /> : null}\n {part}\n </Fragment>\n ));\n}\n\nfunction statusLabel(alarm: AlarmRow): string {\n if (alarm.state === 'CLEARED') return 'Cleared';\n return alarm.acknowledged ? 'Acknowledged' : 'Active';\n}\n\nexport function AlarmTable({ widget, data, actions }: WidgetProps<AlarmStreamState>) {\n const { alarms, total, loading } = data;\n // Actions render only when the runtime supplies a seam AND the viewer may write.\n const canAct = actions?.can('alarm:write') ?? false;\n const [sizeRef, size] = useElementSize<HTMLDivElement>();\n const severity = useSeverityColors();\n const narrow = size.width > 0 && size.width < ALARM_TABLE_NARROW_PX;\n const columns = 5 + (narrow ? 0 : 2) + (canAct ? 1 : 0);\n const text = narrow ? narrowCell : cell;\n // Round the triggering value to a fixed precision when configured; unset keeps the raw\n // value (formatValue's default), matching the table widget. An alarm's raw lastValue is\n // otherwise a full-width float (e.g. 31.19073834583732).\n const precision = optNumber(widget.options, 'precision');\n // Opt-in directional flash on the triggering value, per row (keyed by alarm token).\n const flash = optBoolean(widget.options, 'flashOnChange');\n // Drill: when a `selectionTarget` slot is configured AND the renderer wired a selection\n // callback (view mode, a host that supports it), a device originator becomes a link that\n // re-points that slot \u2014 driving the per-device widgets to the clicked thermostat\n // (ADR-039 selection amendment). Absent either, the originator is plain text.\n const select = useWidgetSelect();\n const timeZone = useWidgetTimeZone();\n const selectionTarget = optString(widget.options, 'selectionTarget');\n const drillTo = select && selectionTarget ? { select, slot: selectionTarget } : undefined;\n\n return (\n <WidgetFrame title={optString(widget.options, 'title')} flush>\n <div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>\n <div ref={sizeRef} style={{ flex: 1, minHeight: 0, overflow: 'auto' }}>\n <table\n style={{\n width: '100%',\n borderCollapse: 'collapse',\n fontSize: fontVar('md'),\n color: css('foreground'),\n fontVariantNumeric: 'tabular-nums',\n }}\n >\n <thead>\n <tr>\n <th style={{ ...headCell, width: 4, padding: 0 }} aria-hidden=\"true\" />\n <th scope=\"col\" style={headCell}>\n Severity\n </th>\n {narrow ? null : (\n <th scope=\"col\" style={headCell}>\n Status\n </th>\n )}\n <th scope=\"col\" style={headCell}>\n Originator\n </th>\n <th scope=\"col\" style={headCell}>\n Alarm\n </th>\n <th scope=\"col\" style={{ ...headCell, textAlign: 'right' }}>\n Value\n </th>\n {narrow ? null : (\n <th scope=\"col\" style={headCell}>\n Raised\n </th>\n )}\n {canAct ? (\n <th scope=\"col\" style={{ ...headCell, textAlign: 'right' }}>\n Actions\n </th>\n ) : null}\n </tr>\n </thead>\n <tbody>\n {alarms.length === 0 ? (\n <tr>\n <td colSpan={columns} style={{ ...cell, textAlign: 'center', color: css('muted-foreground') }}>\n {loading ? 'Loading\u2026' : 'No alarms'}\n </td>\n </tr>\n ) : (\n alarms.map((alarm) => {\n const color = severityColor(severity, alarm.severity);\n return (\n <tr key={alarm.token} style={{ borderBottom: `1px solid ${css('border')}` }}>\n <td aria-hidden=\"true\" style={{ padding: 0, width: 4, background: color }} />\n <td style={cell}>\n <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>\n <span\n aria-hidden=\"true\"\n style={{ width: 8, height: 8, borderRadius: '50%', background: color, flexShrink: 0 }}\n />\n {severityLabel(alarm.severity)}\n </span>\n </td>\n {narrow ? null : <td style={{ ...cell, color: css('muted-foreground') }}>{statusLabel(alarm)}</td>}\n <td style={text}>\n <OriginatorCell alarm={alarm} drillTo={drillTo} />\n </td>\n <td style={text}>\n {hyphenBreaks(alarm.alarmKey)}\n </td>\n <td style={{ ...cell, textAlign: 'right' }}>\n <FlashValue\n value={alarm.lastValue}\n precision={precision}\n enabled={flash}\n identity={alarm.token}\n />\n </td>\n {narrow ? null : (\n <td style={{ ...cell, color: css('muted-foreground') }}>\n {formatDateTime(alarm.raisedTime, timeZone)}\n </td>\n )}\n {canAct ? (\n <td style={{ ...cell, textAlign: 'right' }}>\n {/* actions is defined whenever canAct is true. */}\n <AlarmRowActions alarm={alarm} actions={actions as WidgetActions} />\n </td>\n ) : null}\n </tr>\n );\n })\n )}\n </tbody>\n </table>\n </div>\n {total > alarms.length ? (\n <div\n style={{\n padding: '4px 10px',\n fontSize: fontVar('xs'),\n color: css('muted-foreground'),\n borderTop: `1px solid ${css('border')}`,\n }}\n >\n Showing {alarms.length} of {total}\n </div>\n ) : null}\n </div>\n </WidgetFrame>\n );\n}\n\n// OriginatorCell renders the alarm's originator. When drill is wired AND the originator\n// is a device (it carries a token), it is a link that re-points the target slot to that\n// device; otherwise it is plain text (the token, or the originator type when tokenless).\n// The drill target is a device binding \u2014 a building-scoped alarm table's originators are\n// already members of the context, so the drill stays in-context.\nfunction OriginatorCell({\n alarm,\n drillTo,\n}: {\n alarm: AlarmRow;\n drillTo: { select: WidgetSelect; slot: string } | undefined;\n}) {\n if (drillTo && alarm.originatorType === 'device' && alarm.originatorToken) {\n const token = alarm.originatorToken;\n return (\n <button\n type=\"button\"\n onClick={() => drillTo.select({ slot: drillTo.slot, binding: { kind: 'device', deviceToken: token } })}\n title={`Show ${token} in the linked widgets`}\n style={{\n padding: 0,\n border: 'none',\n background: 'none',\n font: 'inherit',\n color: css('primary'),\n cursor: 'pointer',\n textDecoration: 'underline',\n textUnderlineOffset: 2,\n }}\n >\n {hyphenBreaks(token)}\n </button>\n );\n }\n return <>{hyphenBreaks(alarm.originatorToken ?? alarm.originatorType)}</>;\n}\n\nconst actionButton: CSSProperties = {\n padding: '2px 8px',\n fontSize: fontVar('sm'),\n lineHeight: 1.4,\n borderRadius: 4,\n border: `1px solid ${css('border')}`,\n background: css('card'),\n color: css('foreground'),\n cursor: 'pointer',\n};\n\n// AlarmRowActions owns the pending/error state for one row's Acknowledge / Clear. Only\n// an ACTIVE alarm is actionable (a cleared \u2014 or any future non-active \u2014 state shows\n// \"\u2014\"); an acknowledged one still shows Clear. After a successful mutation the hub\n// reconciles the table, so the row updates itself.\nfunction AlarmRowActions({ alarm, actions }: { alarm: AlarmRow; actions: WidgetActions }) {\n const severity = useSeverityColors();\n const [pending, setPending] = useState<'ack' | 'clear' | null>(null);\n const [failed, setFailed] = useState(false);\n\n if (alarm.state !== 'ACTIVE') {\n return <span style={{ color: css('muted-foreground') }}>\u2014</span>;\n }\n\n const run = (kind: 'ack' | 'clear', op: () => Promise<void>) => {\n setPending(kind);\n setFailed(false);\n // Call op() synchronously (so an immediate dispatch is observable) but guard a\n // synchronous throw too, so a misbehaving seam can't leave the row stuck-disabled.\n let promise: Promise<void>;\n try {\n promise = op();\n } catch {\n setFailed(true);\n setPending(null);\n return;\n }\n promise.catch(() => setFailed(true)).finally(() => setPending(null));\n };\n\n return (\n <span style={{ display: 'inline-flex', gap: 6, justifyContent: 'flex-end', alignItems: 'center' }}>\n {failed ? (\n <span\n role=\"img\"\n aria-label=\"Action failed \u2014 retry\"\n title=\"Action failed \u2014 retry\"\n style={{ color: severityColor(severity, 'CRITICAL'), fontSize: fontVar('sm') }}\n >\n \u26A0\n </span>\n ) : null}\n {!alarm.acknowledged ? (\n <button\n type=\"button\"\n style={{ ...actionButton, opacity: pending ? 0.6 : 1 }}\n disabled={pending !== null}\n aria-busy={pending === 'ack'}\n aria-label={`Acknowledge alarm ${alarm.alarmKey}`}\n onClick={() => run('ack', () => actions.acknowledgeAlarm(alarm.token))}\n >\n {pending === 'ack' ? '\u2026' : 'Ack'}\n </button>\n ) : null}\n <button\n type=\"button\"\n style={{ ...actionButton, opacity: pending ? 0.6 : 1 }}\n disabled={pending !== null}\n aria-busy={pending === 'clear'}\n aria-label={`Clear alarm ${alarm.alarmKey}`}\n onClick={() => run('clear', () => actions.clearAlarm(alarm.token))}\n >\n {pending === 'clear' ? '\u2026' : 'Clear'}\n </button>\n </span>\n );\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// alarm-count \u2014 a KPI card of how many alarms match the widget's scope+filter\n// (ADR-041). The number is the server total (past the loaded page); its color is the\n// most severe among the CURRENTLY-LOADED alarms (the newest page), and the worst\n// severity is also spelled out as text/aria so the signal never reads on hue alone (a\n// color-blind or screen-reader viewer still gets it). At zero it stays muted.\n\nimport {\n type AlarmStreamState,\n css,\n fitValueFont,\n fitValueFontCss,\n FIT_MIN_FONT_PX,\n fontVar,\n optString,\n spaceVar,\n useSeverityColors,\n useStickyChars,\n useWidgetScale,\n WidgetFrame,\n type WidgetProps,\n} from '@devicechain/widget-sdk';\n\nimport { highestSeverity, severityColor, severityLabel } from './severity';\n\n// The caption under the count (11 px text plus its 6 px margin). It always shows -- it names\n// the worst severity -- so the count is fitted to what is left rather than pushing it out.\nexport const ALARM_COUNT_CAPTION_ROOM_PX = 20;\n\n// Rendered INSIDE the frame: the widget scale is the frame's content column, which is only in\n// context below the frame.\nfunction AlarmCountBody({ data }: { data: AlarmStreamState }) {\n const { size, scale } = useWidgetScale();\n const severity = useSeverityColors();\n const { total, alarms, loading } = data;\n\n const worst = highestSeverity(alarms.map((a) => a.severity));\n // At zero (or before any alarm is loaded) stay neutral; otherwise take the worst\n // loaded severity's color as the alert accent.\n const alerting = total > 0 && worst != null;\n const color = alerting ? severityColor(severity, worst) : css('muted-foreground');\n\n const display = loading && total === 0 ? '\u2014' : String(total);\n const chars = useStickyChars(display.length);\n const floor = Math.round(FIT_MIN_FONT_PX * scale);\n const measured = size.width > 0 && size.height > 0;\n const valueFont = measured\n ? fitValueFont(\n { width: size.width, height: Math.max(1, size.height - ALARM_COUNT_CAPTION_ROOM_PX * scale) },\n chars,\n { floor },\n )\n : // First frame: the same curve in cq units, resolved by the browser against the frame's\n // column (it does not take the caption's room off; the first measure does).\n fitValueFontCss(chars, { floor });\n const unit = total === 1 ? 'alarm' : 'alarms';\n const ariaLabel = alerting ? `${total} ${unit}, worst severity ${severityLabel(worst)}` : `${total} ${unit}`;\n\n return (\n <div\n style={{\n display: 'flex',\n flexDirection: 'column',\n justifyContent: 'safe center',\n alignItems: 'center',\n height: '100%',\n boxSizing: 'border-box',\n overflow: 'hidden',\n textAlign: 'center',\n }}\n >\n <div\n aria-label={ariaLabel}\n style={{\n fontSize: valueFont,\n fontWeight: 700,\n lineHeight: 1,\n whiteSpace: 'nowrap',\n fontVariantNumeric: 'tabular-nums',\n color,\n }}\n >\n {display}\n </div>\n <div\n style={{\n fontSize: fontVar('xs'),\n color: css('muted-foreground'),\n marginTop: spaceVar(1),\n textTransform: 'uppercase',\n letterSpacing: '0.05em',\n }}\n >\n <span>{unit}</span>\n {alerting ? <span>{` \u00B7 worst ${severityLabel(worst)}`}</span> : null}\n </div>\n </div>\n );\n}\n\nexport function AlarmCount({ widget, data }: WidgetProps<AlarmStreamState>) {\n return (\n <WidgetFrame title={optString(widget.options, 'title')}>\n <AlarmCountBody data={data} />\n </WidgetFrame>\n );\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["css", "Fragment", "css", "css", "fontVar", "optString", "useSeverityColors", "WidgetFrame", "jsx", "jsxs", "useSeverityColors", "css", "fontVar", "WidgetFrame", "optString"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// src/access-strings.ts
|
|
2
|
+
import { viewerLocale } from "@devicechain/widget-sdk";
|
|
3
|
+
var ACCESS_STRINGS = {
|
|
4
|
+
en: { forbidden: "You do not have access to this data" },
|
|
5
|
+
es: { forbidden: "No tiene acceso a estos datos" }
|
|
6
|
+
};
|
|
7
|
+
function accessStrings(locale = viewerLocale()) {
|
|
8
|
+
return locale?.toLowerCase().split("-")[0] === "es" ? ACCESS_STRINGS.es : ACCESS_STRINGS.en;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
ACCESS_STRINGS,
|
|
13
|
+
accessStrings
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=chunk-BRH66T5F.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/access-strings.ts"],
|
|
4
|
+
"sourcesContent": ["// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// The one sentence every widget shows when the platform REFUSES what it reads \u2014 a member whose\n// access is limited to part of the tenant viewing a widget bound to something outside it \u2014 in\n// place of the error frame's \"Data unavailable\" and the server's raw \"forbidden: \u2026\" text. It is\n// a refusal working as designed, not an outage, so it reads as a state. Kept apart from the\n// per-widget tables so the eager error frame and the lazily loaded fleet chunk share it without\n// either pulling in the other's strings.\n\nimport { viewerLocale } from '@devicechain/widget-sdk';\n\nexport const ACCESS_STRINGS: Record<'en' | 'es', { forbidden: string }> = {\n en: { forbidden: 'You do not have access to this data' },\n es: { forbidden: 'No tiene acceso a estos datos' },\n};\n\nexport function accessStrings(locale: string | undefined = viewerLocale()): { forbidden: string } {\n return locale?.toLowerCase().split('-')[0] === 'es' ? ACCESS_STRINGS.es : ACCESS_STRINGS.en;\n}\n"],
|
|
5
|
+
"mappings": ";AAUA,SAAS,oBAAoB;AAEtB,IAAM,iBAA6D;AAAA,EACxE,IAAI,EAAE,WAAW,sCAAsC;AAAA,EACvD,IAAI,EAAE,WAAW,gCAAgC;AACnD;AAEO,SAAS,cAAc,SAA6B,aAAa,GAA0B;AAChG,SAAO,QAAQ,YAAY,EAAE,MAAM,GAAG,EAAE,CAAC,MAAM,OAAO,eAAe,KAAK,eAAe;AAC3F;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// src/widgets/entity-selector.tsx
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import {
|
|
4
|
+
css,
|
|
5
|
+
fontVar,
|
|
6
|
+
optString,
|
|
7
|
+
useCandidates,
|
|
8
|
+
useWidgetSelect,
|
|
9
|
+
WidgetFrame
|
|
10
|
+
} from "@devicechain/widget-sdk";
|
|
11
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
12
|
+
function tokenOf(b) {
|
|
13
|
+
return b.kind === "device" ? b.deviceToken : b.anchor.targetToken;
|
|
14
|
+
}
|
|
15
|
+
var control = {
|
|
16
|
+
width: "100%",
|
|
17
|
+
fontSize: fontVar("md"),
|
|
18
|
+
padding: "8px 10px",
|
|
19
|
+
borderRadius: 6,
|
|
20
|
+
border: `1px solid ${css("border")}`,
|
|
21
|
+
background: css("background"),
|
|
22
|
+
color: css("foreground")
|
|
23
|
+
};
|
|
24
|
+
var hint = {
|
|
25
|
+
fontSize: fontVar("sm"),
|
|
26
|
+
color: css("muted-foreground")
|
|
27
|
+
};
|
|
28
|
+
function EntitySelector({ widget }) {
|
|
29
|
+
const select = useWidgetSelect();
|
|
30
|
+
const target = optString(widget.options, "selectionTarget");
|
|
31
|
+
const { candidates, loading, error, wired } = useCandidates(target);
|
|
32
|
+
const title = optString(widget.options, "title");
|
|
33
|
+
const [pending, setPending] = useState(null);
|
|
34
|
+
const selected = candidates.find((c) => c.selected);
|
|
35
|
+
const selectedToken = selected ? tokenOf(selected.binding) : "";
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (pending !== null && (selectedToken === pending || !candidates.some((c) => tokenOf(c.binding) === pending))) {
|
|
38
|
+
setPending(null);
|
|
39
|
+
}
|
|
40
|
+
}, [candidates, selectedToken, pending]);
|
|
41
|
+
const inert = !select || !target || !wired;
|
|
42
|
+
const value = pending ?? selectedToken;
|
|
43
|
+
const onChange = (e) => {
|
|
44
|
+
const token = e.target.value;
|
|
45
|
+
if (!token) return;
|
|
46
|
+
const candidate = candidates.find((c) => tokenOf(c.binding) === token);
|
|
47
|
+
if (select && target && candidate) {
|
|
48
|
+
setPending(token);
|
|
49
|
+
select({ slot: target, binding: candidate.binding });
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
return /* @__PURE__ */ jsx(WidgetFrame, { title, children: /* @__PURE__ */ jsx(
|
|
53
|
+
"div",
|
|
54
|
+
{
|
|
55
|
+
style: {
|
|
56
|
+
display: "flex",
|
|
57
|
+
flexDirection: "column",
|
|
58
|
+
gap: 8,
|
|
59
|
+
height: "100%",
|
|
60
|
+
boxSizing: "border-box",
|
|
61
|
+
overflow: "auto"
|
|
62
|
+
},
|
|
63
|
+
children: inert ? /* @__PURE__ */ jsx("span", { style: hint, children: select && target ? "Selection isn\u2019t available here." : select ? "No target slot configured." : "Selection is available on the live dashboard." }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
64
|
+
/* @__PURE__ */ jsxs(
|
|
65
|
+
"select",
|
|
66
|
+
{
|
|
67
|
+
style: { ...control, opacity: loading ? 0.6 : 1 },
|
|
68
|
+
value,
|
|
69
|
+
disabled: candidates.length === 0,
|
|
70
|
+
onChange,
|
|
71
|
+
"aria-label": title || `Select ${target}`,
|
|
72
|
+
children: [
|
|
73
|
+
/* @__PURE__ */ jsx("option", { value: "", disabled: true, children: loading ? "Loading\u2026" : candidates.length === 0 ? "No options" : "Select\u2026" }),
|
|
74
|
+
candidates.map((c) => /* @__PURE__ */ jsx("option", { value: tokenOf(c.binding), children: c.label }, tokenOf(c.binding)))
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
),
|
|
78
|
+
error ? /* @__PURE__ */ jsx("span", { style: hint, children: "Couldn\u2019t load options." }) : null
|
|
79
|
+
] })
|
|
80
|
+
}
|
|
81
|
+
) });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export {
|
|
85
|
+
EntitySelector
|
|
86
|
+
};
|
|
87
|
+
//# sourceMappingURL=chunk-FM7OI5TQ.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/widgets/entity-selector.tsx"],
|
|
4
|
+
"sourcesContent": ["// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// entity-selector \u2014 a flat picker that re-points a target slot's binding (ADR-039 selection\n// amendment). It is the authored companion to the alarm-originator drill: the viewer chooses\n// WHICH entity a slot binds to, and every widget bound to that slot (and its scoped children,\n// via the cascade) follows. Two shapes, both from `options.selectionTarget`:\n// \u2022 target is a SCOPED child slot \u2192 a member picker (which thermostat within the building).\n// \u2022 target is a ROOT slot \u2192 a context picker (which building the dashboard shows).\n// The candidate set comes from the ambient provider (useCandidates), which the host computes\n// from the target slot's scope; the pick fires the ambient select() callback. Inert (a muted\n// hint) wherever selection isn't wired \u2014 edit mode, preview, a read-only embedder.\n\nimport type { SlotBinding } from '@devicechain/dashboards';\nimport { useEffect, useState, type ChangeEvent, type CSSProperties } from 'react';\n\nimport {\n css,\n fontVar,\n optString,\n useCandidates,\n useWidgetSelect,\n WidgetFrame,\n type WidgetProps,\n} from '@devicechain/widget-sdk';\n\n\n// A binding's stable option value \u2014 the device token or the anchor's target token. Used as\n// the <option> value (not the array index) so the control's value survives a re-fetch that\n// reorders/replaces candidates.\nfunction tokenOf(b: SlotBinding): string {\n return b.kind === 'device' ? b.deviceToken : b.anchor.targetToken;\n}\n\nconst control: CSSProperties = {\n width: '100%',\n fontSize: fontVar('md'),\n padding: '8px 10px',\n borderRadius: 6,\n border: `1px solid ${css('border')}`,\n background: css('background'),\n color: css('foreground'),\n};\n\nconst hint: CSSProperties = {\n fontSize: fontVar('sm'),\n color: css('muted-foreground'),\n};\n\nexport function EntitySelector({ widget }: WidgetProps<null>) {\n const select = useWidgetSelect();\n const target = optString(widget.options, 'selectionTarget');\n const { candidates, loading, error, wired } = useCandidates(target);\n const title = optString(widget.options, 'title');\n\n // Optimistic pick: hold the just-chosen token so the control reflects the choice\n // immediately \u2014 through the (on a scoped dashboard, two-hop) async re-resolve \u2014 rather\n // than snapping back to the prior selection until the refetch settles. Cleared once the\n // fetched candidates reflect it, or it is no longer offered (the pick was dropped).\n const [pending, setPending] = useState<string | null>(null);\n const selected = candidates.find((c) => c.selected);\n const selectedToken = selected ? tokenOf(selected.binding) : '';\n useEffect(() => {\n if (pending !== null && (selectedToken === pending || !candidates.some((c) => tokenOf(c.binding) === pending))) {\n setPending(null);\n }\n }, [candidates, selectedToken, pending]);\n\n // Inert wherever selection isn't wired (no select callback OR no candidate provider) or no\n // target slot is authored \u2014 the picker would drive nothing, so show a muted explanation.\n const inert = !select || !target || !wired;\n const value = pending ?? selectedToken;\n\n const onChange = (e: ChangeEvent<HTMLSelectElement>) => {\n const token = e.target.value;\n if (!token) return; // the disabled placeholder ('') \u2014 nothing to select\n const candidate = candidates.find((c) => tokenOf(c.binding) === token);\n if (select && target && candidate) {\n setPending(token);\n select({ slot: target, binding: candidate.binding });\n }\n };\n\n return (\n <WidgetFrame title={title}>\n <div\n style={{\n display: 'flex',\n flexDirection: 'column',\n gap: 8,\n height: '100%',\n boxSizing: 'border-box',\n overflow: 'auto',\n }}\n >\n {inert ? (\n <span style={hint}>\n {select && target ? 'Selection isn\u2019t available here.' : select ? 'No target slot configured.' : 'Selection is available on the live dashboard.'}\n </span>\n ) : (\n <>\n <select\n style={{ ...control, opacity: loading ? 0.6 : 1 }}\n value={value}\n disabled={candidates.length === 0}\n onChange={onChange}\n aria-label={title || `Select ${target}`}\n >\n {/* A blank option so an unbound slot (no member picked yet, or the prior pick\n is out of the new context) shows a prompt rather than defaulting to a row. */}\n <option value=\"\" disabled>\n {loading ? 'Loading\u2026' : candidates.length === 0 ? 'No options' : 'Select\u2026'}\n </option>\n {candidates.map((c) => (\n <option key={tokenOf(c.binding)} value={tokenOf(c.binding)}>\n {c.label}\n </option>\n ))}\n </select>\n {error ? <span style={hint}>Couldn\u2019t load options.</span> : null}\n </>\n )}\n </div>\n </WidgetFrame>\n );\n}\n"],
|
|
5
|
+
"mappings": ";AAcA,SAAS,WAAW,gBAAsD;AAE1E;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AAwEG,SAIA,UAJA,KAKE,YALF;AAlEV,SAAS,QAAQ,GAAwB;AACvC,SAAO,EAAE,SAAS,WAAW,EAAE,cAAc,EAAE,OAAO;AACxD;AAEA,IAAM,UAAyB;AAAA,EAC7B,OAAO;AAAA,EACP,UAAU,QAAQ,IAAI;AAAA,EACtB,SAAS;AAAA,EACT,cAAc;AAAA,EACd,QAAQ,aAAa,IAAI,QAAQ,CAAC;AAAA,EAClC,YAAY,IAAI,YAAY;AAAA,EAC5B,OAAO,IAAI,YAAY;AACzB;AAEA,IAAM,OAAsB;AAAA,EAC1B,UAAU,QAAQ,IAAI;AAAA,EACtB,OAAO,IAAI,kBAAkB;AAC/B;AAEO,SAAS,eAAe,EAAE,OAAO,GAAsB;AAC5D,QAAM,SAAS,gBAAgB;AAC/B,QAAM,SAAS,UAAU,OAAO,SAAS,iBAAiB;AAC1D,QAAM,EAAE,YAAY,SAAS,OAAO,MAAM,IAAI,cAAc,MAAM;AAClE,QAAM,QAAQ,UAAU,OAAO,SAAS,OAAO;AAM/C,QAAM,CAAC,SAAS,UAAU,IAAI,SAAwB,IAAI;AAC1D,QAAM,WAAW,WAAW,KAAK,CAAC,MAAM,EAAE,QAAQ;AAClD,QAAM,gBAAgB,WAAW,QAAQ,SAAS,OAAO,IAAI;AAC7D,YAAU,MAAM;AACd,QAAI,YAAY,SAAS,kBAAkB,WAAW,CAAC,WAAW,KAAK,CAAC,MAAM,QAAQ,EAAE,OAAO,MAAM,OAAO,IAAI;AAC9G,iBAAW,IAAI;AAAA,IACjB;AAAA,EACF,GAAG,CAAC,YAAY,eAAe,OAAO,CAAC;AAIvC,QAAM,QAAQ,CAAC,UAAU,CAAC,UAAU,CAAC;AACrC,QAAM,QAAQ,WAAW;AAEzB,QAAM,WAAW,CAAC,MAAsC;AACtD,UAAM,QAAQ,EAAE,OAAO;AACvB,QAAI,CAAC,MAAO;AACZ,UAAM,YAAY,WAAW,KAAK,CAAC,MAAM,QAAQ,EAAE,OAAO,MAAM,KAAK;AACrE,QAAI,UAAU,UAAU,WAAW;AACjC,iBAAW,KAAK;AAChB,aAAO,EAAE,MAAM,QAAQ,SAAS,UAAU,QAAQ,CAAC;AAAA,IACrD;AAAA,EACF;AAEA,SACE,oBAAC,eAAY,OACX;AAAA,IAAC;AAAA;AAAA,MACC,OAAO;AAAA,QACL,SAAS;AAAA,QACT,eAAe;AAAA,QACf,KAAK;AAAA,QACL,QAAQ;AAAA,QACR,WAAW;AAAA,QACX,UAAU;AAAA,MACZ;AAAA,MAEC,kBACC,oBAAC,UAAK,OAAO,MACV,oBAAU,SAAS,yCAAoC,SAAS,+BAA+B,iDAClG,IAEA,iCACE;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,OAAO,EAAE,GAAG,SAAS,SAAS,UAAU,MAAM,EAAE;AAAA,YAChD;AAAA,YACA,UAAU,WAAW,WAAW;AAAA,YAChC;AAAA,YACA,cAAY,SAAS,UAAU,MAAM;AAAA,YAIrC;AAAA,kCAAC,YAAO,OAAM,IAAG,UAAQ,MACtB,oBAAU,kBAAa,WAAW,WAAW,IAAI,eAAe,gBACnE;AAAA,cACC,WAAW,IAAI,CAAC,MACf,oBAAC,YAAgC,OAAO,QAAQ,EAAE,OAAO,GACtD,YAAE,SADQ,QAAQ,EAAE,OAAO,CAE9B,CACD;AAAA;AAAA;AAAA,QACH;AAAA,QACC,QAAQ,oBAAC,UAAK,OAAO,MAAM,yCAAsB,IAAU;AAAA,SAC9D;AAAA;AAAA,EAEJ,GACF;AAEJ;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// src/widgets/table.tsx
|
|
2
|
+
import {
|
|
3
|
+
css,
|
|
4
|
+
fontVar,
|
|
5
|
+
spaceVar,
|
|
6
|
+
FlashValue,
|
|
7
|
+
formatTimestamp,
|
|
8
|
+
optBoolean,
|
|
9
|
+
optNumber,
|
|
10
|
+
optString,
|
|
11
|
+
useWidgetTimeZone,
|
|
12
|
+
WidgetFrame
|
|
13
|
+
} from "@devicechain/widget-sdk";
|
|
14
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
15
|
+
var cell = { padding: `${spaceVar(1)} ${spaceVar(3)}`, textAlign: "left" };
|
|
16
|
+
var headCell = {
|
|
17
|
+
...cell,
|
|
18
|
+
fontSize: fontVar("xs"),
|
|
19
|
+
fontWeight: 600,
|
|
20
|
+
textTransform: "uppercase",
|
|
21
|
+
letterSpacing: "0.04em",
|
|
22
|
+
color: css("muted-foreground"),
|
|
23
|
+
borderBottom: `1px solid ${css("border")}`
|
|
24
|
+
};
|
|
25
|
+
function Table({ widget, data }) {
|
|
26
|
+
const timeZone = useWidgetTimeZone();
|
|
27
|
+
const rows = Object.values(data.latest).sort((a, b) => a.name.localeCompare(b.name));
|
|
28
|
+
const precision = optNumber(widget.options, "precision");
|
|
29
|
+
const flash = optBoolean(widget.options, "flashOnChange");
|
|
30
|
+
return /* @__PURE__ */ jsx(WidgetFrame, { title: optString(widget.options, "title"), flush: true, children: /* @__PURE__ */ jsx("div", { style: { height: "100%", overflow: "auto" }, children: /* @__PURE__ */ jsxs("table", { style: { width: "100%", borderCollapse: "collapse", fontSize: fontVar("md"), color: css("foreground") }, children: [
|
|
31
|
+
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
32
|
+
/* @__PURE__ */ jsx("th", { style: headCell, children: "Measurement" }),
|
|
33
|
+
/* @__PURE__ */ jsx("th", { style: headCell, children: "Value" }),
|
|
34
|
+
/* @__PURE__ */ jsx("th", { style: headCell, children: "Time" })
|
|
35
|
+
] }) }),
|
|
36
|
+
/* @__PURE__ */ jsx("tbody", { children: rows.length === 0 ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx("td", { colSpan: 3, style: { ...cell, textAlign: "center", color: css("muted-foreground") }, children: "No data yet" }) }) : rows.map((row) => /* @__PURE__ */ jsxs("tr", { children: [
|
|
37
|
+
/* @__PURE__ */ jsx("td", { style: cell, children: row.name }),
|
|
38
|
+
/* @__PURE__ */ jsx("td", { style: { ...cell, fontVariantNumeric: "tabular-nums" }, children: /* @__PURE__ */ jsx(
|
|
39
|
+
FlashValue,
|
|
40
|
+
{
|
|
41
|
+
value: row.value,
|
|
42
|
+
precision,
|
|
43
|
+
enabled: flash,
|
|
44
|
+
identity: `${row.deviceToken}:${row.name}`
|
|
45
|
+
}
|
|
46
|
+
) }),
|
|
47
|
+
/* @__PURE__ */ jsx("td", { style: { ...cell, color: css("muted-foreground") }, children: formatTimestamp(row.occurredTime, timeZone) })
|
|
48
|
+
] }, row.name)) })
|
|
49
|
+
] }) }) });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export {
|
|
53
|
+
Table
|
|
54
|
+
};
|
|
55
|
+
//# sourceMappingURL=chunk-USRYKW2J.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/widgets/table.tsx"],
|
|
4
|
+
"sourcesContent": ["// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// table \u2014 the latest value of every measurement the datasource carries, one row\n// per measurement name.\n\nimport type { CSSProperties } from 'react';\n\nimport {\n css,\n fontVar,\n spaceVar,\n FlashValue,\n formatTimestamp,\n optBoolean,\n optNumber,\n optString,\n useWidgetTimeZone,\n WidgetFrame,\n type WidgetProps,\n} from '@devicechain/widget-sdk';\n\n\nconst cell: CSSProperties = { padding: `${spaceVar(1)} ${spaceVar(3)}`, textAlign: 'left' };\nconst headCell: CSSProperties = {\n ...cell,\n fontSize: fontVar('xs'),\n fontWeight: 600,\n textTransform: 'uppercase',\n letterSpacing: '0.04em',\n color: css('muted-foreground'),\n borderBottom: `1px solid ${css('border')}`,\n};\n\nexport function Table({ widget, data }: WidgetProps) {\n const timeZone = useWidgetTimeZone();\n const rows = Object.values(data.latest).sort((a, b) => a.name.localeCompare(b.name));\n // Round to a fixed number of decimals when configured; unset keeps the raw value\n // (formatValue's default). A live sensor value is otherwise a wall of digits.\n const precision = optNumber(widget.options, 'precision');\n // Opt-in directional flash per row (keyed by measurement name, so each value's cue is\n // independent \u2014 see FlashValue).\n const flash = optBoolean(widget.options, 'flashOnChange');\n\n return (\n <WidgetFrame title={optString(widget.options, 'title')} flush>\n <div style={{ height: '100%', overflow: 'auto' }}>\n <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: fontVar('md'), color: css('foreground') }}>\n <thead>\n <tr>\n <th style={headCell}>Measurement</th>\n <th style={headCell}>Value</th>\n <th style={headCell}>Time</th>\n </tr>\n </thead>\n <tbody>\n {rows.length === 0 ? (\n <tr>\n <td colSpan={3} style={{ ...cell, textAlign: 'center', color: css('muted-foreground') }}>\n No data yet\n </td>\n </tr>\n ) : (\n rows.map((row) => (\n <tr key={row.name}>\n <td style={cell}>{row.name}</td>\n <td style={{ ...cell, fontVariantNumeric: 'tabular-nums' }}>\n <FlashValue\n value={row.value}\n precision={precision}\n enabled={flash}\n identity={`${row.deviceToken}:${row.name}`}\n />\n {/* classifier is deliberately NOT shown: it is the bound metric\n definition's internal id (ADR-016), not a user-facing label \u2014\n appending it rendered as spurious digits after the value. */}\n </td>\n <td style={{ ...cell, color: css('muted-foreground') }}>{formatTimestamp(row.occurredTime, timeZone)}</td>\n </tr>\n ))\n )}\n </tbody>\n </table>\n </div>\n </WidgetFrame>\n );\n}\n"],
|
|
5
|
+
"mappings": ";AAQA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AA6BK,SACE,KADF;AA1BZ,IAAM,OAAsB,EAAE,SAAS,GAAG,SAAS,CAAC,CAAC,IAAI,SAAS,CAAC,CAAC,IAAI,WAAW,OAAO;AAC1F,IAAM,WAA0B;AAAA,EAC9B,GAAG;AAAA,EACH,UAAU,QAAQ,IAAI;AAAA,EACtB,YAAY;AAAA,EACZ,eAAe;AAAA,EACf,eAAe;AAAA,EACf,OAAO,IAAI,kBAAkB;AAAA,EAC7B,cAAc,aAAa,IAAI,QAAQ,CAAC;AAC1C;AAEO,SAAS,MAAM,EAAE,QAAQ,KAAK,GAAgB;AACnD,QAAM,WAAW,kBAAkB;AACnC,QAAM,OAAO,OAAO,OAAO,KAAK,MAAM,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,KAAK,cAAc,EAAE,IAAI,CAAC;AAGnF,QAAM,YAAY,UAAU,OAAO,SAAS,WAAW;AAGvD,QAAM,QAAQ,WAAW,OAAO,SAAS,eAAe;AAExD,SACE,oBAAC,eAAY,OAAO,UAAU,OAAO,SAAS,OAAO,GAAG,OAAK,MAC3D,8BAAC,SAAI,OAAO,EAAE,QAAQ,QAAQ,UAAU,OAAO,GAC7C,+BAAC,WAAM,OAAO,EAAE,OAAO,QAAQ,gBAAgB,YAAY,UAAU,QAAQ,IAAI,GAAG,OAAO,IAAI,YAAY,EAAE,GAC3G;AAAA,wBAAC,WACC,+BAAC,QACC;AAAA,0BAAC,QAAG,OAAO,UAAU,yBAAW;AAAA,MAChC,oBAAC,QAAG,OAAO,UAAU,mBAAK;AAAA,MAC1B,oBAAC,QAAG,OAAO,UAAU,kBAAI;AAAA,OAC3B,GACF;AAAA,IACA,oBAAC,WACE,eAAK,WAAW,IACf,oBAAC,QACC,8BAAC,QAAG,SAAS,GAAG,OAAO,EAAE,GAAG,MAAM,WAAW,UAAU,OAAO,IAAI,kBAAkB,EAAE,GAAG,yBAEzF,GACF,IAEA,KAAK,IAAI,CAAC,QACR,qBAAC,QACC;AAAA,0BAAC,QAAG,OAAO,MAAO,cAAI,MAAK;AAAA,MAC3B,oBAAC,QAAG,OAAO,EAAE,GAAG,MAAM,oBAAoB,eAAe,GACvD;AAAA,QAAC;AAAA;AAAA,UACC,OAAO,IAAI;AAAA,UACX;AAAA,UACA,SAAS;AAAA,UACT,UAAU,GAAG,IAAI,WAAW,IAAI,IAAI,IAAI;AAAA;AAAA,MAC1C,GAIF;AAAA,MACA,oBAAC,QAAG,OAAO,EAAE,GAAG,MAAM,OAAO,IAAI,kBAAkB,EAAE,GAAI,0BAAgB,IAAI,cAAc,QAAQ,GAAE;AAAA,SAb9F,IAAI,IAcb,CACD,GAEL;AAAA,KACF,GACF,GACF;AAEJ;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|