@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,775 @@
1
+ import {
2
+ buildPayload,
3
+ commandStatusColor,
4
+ commandStatusLabel,
5
+ isScalar,
6
+ parseParameterSchema,
7
+ validateParams
8
+ } from "./chunk-YPFB6DAL.js";
9
+
10
+ // src/widgets/command-slider.tsx
11
+ import { useEffect as useEffect2, useId, useMemo, useRef as useRef2, useState as useState2 } from "react";
12
+ import {
13
+ css as css2,
14
+ FIT_MIN_FONT_PX,
15
+ fitValueFont,
16
+ fitValueFontCss,
17
+ fontVar as fontVar2,
18
+ optNumber,
19
+ optString,
20
+ spaceVar as spaceVar2,
21
+ useStickyChars,
22
+ WidgetFrame
23
+ } from "@devicechain/widget-sdk";
24
+
25
+ // src/command-control-core.ts
26
+ var refuse = (refusal) => ({ ok: false, refusal });
27
+ var DEFAULT_NOTCHES = 100;
28
+ function bindControl(kind, input) {
29
+ if (!input.commandName) return refuse({ code: "noCommand" });
30
+ const params = parseParameterSchema(input.parameterSchema);
31
+ const wanted = (p) => isScalar(p) && (kind === "boolean" ? p.dataType === "BOOLEAN" : p.dataType === "INT" || p.dataType === "DOUBLE");
32
+ let param;
33
+ if (input.parameter) {
34
+ param = params.find((p) => p.name === input.parameter);
35
+ if (!param) return refuse({ code: "parameterMissing", name: input.parameter });
36
+ if (!wanted(param)) return refuse({ code: "wrongType", name: param.name, kind });
37
+ } else {
38
+ const candidates = params.filter(wanted);
39
+ if (candidates.length !== 1) return refuse({ code: "setParameter" });
40
+ param = candidates[0];
41
+ }
42
+ if (param.enum && param.enum.length > 0) return refuse({ code: "fixedChoices", name: param.name });
43
+ const others = params.filter((p) => p !== param);
44
+ const unsatisfied = others.filter((p) => p.required && (!isScalar(p) || p.default == null || p.default === ""));
45
+ if (unsatisfied.length > 0) return refuse({ code: "otherRequired", names: unsatisfied.map((p) => p.name) });
46
+ const defaulted = others.filter((p) => isScalar(p) && p.default != null && p.default !== "");
47
+ const invalid = new Set(
48
+ Object.keys(validateParams(defaulted, Object.fromEntries(defaulted.map((p) => [p.name, p.default ?? ""]))))
49
+ );
50
+ for (const p of defaulted) if (p.enum && p.enum.length > 0 && !p.enum.includes(p.default ?? "")) invalid.add(p.name);
51
+ if (invalid.size > 0) return refuse({ code: "badDefault", names: defaulted.filter((p) => invalid.has(p.name)).map((p) => p.name) });
52
+ const sent = [param, ...defaulted];
53
+ if (kind === "boolean") return { ok: true, kind, param, sent };
54
+ const min = param.minValue ?? input.min;
55
+ const max = param.maxValue ?? input.max;
56
+ if (min === void 0 || max === void 0) return refuse({ code: "setRange" });
57
+ if (!(max > min)) return refuse({ code: "badRange" });
58
+ const integer = param.dataType === "INT";
59
+ const step = input.step !== void 0 && input.step > 0 ? input.step : integer ? 1 : (max - min) / DEFAULT_NOTCHES;
60
+ return { ok: true, kind, param, range: { min, max, step, integer }, sent };
61
+ }
62
+ function decimalsOf(n) {
63
+ const text = String(n);
64
+ const e = /e-(\d+)$/.exec(text);
65
+ if (e) return Number(e[1]);
66
+ const dot = text.indexOf(".");
67
+ return dot < 0 ? 0 : text.length - dot - 1;
68
+ }
69
+ function clampToRange(range, raw) {
70
+ const finite = Number.isFinite(raw) ? raw : range.min;
71
+ const bounded = Math.min(range.max, Math.max(range.min, finite));
72
+ const steps = Math.round((bounded - range.min) / range.step);
73
+ const decimals = Math.max(decimalsOf(range.step), decimalsOf(range.min));
74
+ let snapped = Number((range.min + steps * range.step).toFixed(Math.min(decimals, 12)));
75
+ if (snapped > range.max) snapped = Number((snapped - range.step).toFixed(Math.min(decimals, 12)));
76
+ if (range.integer) snapped = Math.round(snapped);
77
+ return Math.min(range.max, Math.max(range.min, snapped));
78
+ }
79
+ function payloadFor(binding, value) {
80
+ const values = {};
81
+ for (const p of binding.sent) {
82
+ values[p.name] = p.default ?? "";
83
+ if (p === binding.param) values[p.name] = String(value);
84
+ }
85
+ const payload = buildPayload(binding.sent, values);
86
+ return payload;
87
+ }
88
+ function deliveryGroup(status) {
89
+ switch (status) {
90
+ case "QUEUED":
91
+ case "HELD":
92
+ case "SENT":
93
+ case "PARKED":
94
+ return "pending";
95
+ case "SUCCESSFUL":
96
+ return "delivered";
97
+ case "FAILED":
98
+ case "TIMEOUT":
99
+ case "EXPIRED":
100
+ case "CANCELLED":
101
+ return "failed";
102
+ default:
103
+ return "unknown";
104
+ }
105
+ }
106
+ function readValue(kind, payload, name) {
107
+ if (!payload) return void 0;
108
+ let parsed;
109
+ try {
110
+ parsed = JSON.parse(payload);
111
+ } catch {
112
+ return void 0;
113
+ }
114
+ if (typeof parsed !== "object" || parsed === null) return void 0;
115
+ const v = parsed[name];
116
+ if (kind === "number") return typeof v === "number" && Number.isFinite(v) ? v : void 0;
117
+ if (typeof v === "boolean") return v;
118
+ return void 0;
119
+ }
120
+ function lastSent(kind, commands, commandName, parameter, optimistic) {
121
+ let newest;
122
+ for (const c of commands) {
123
+ if (c.name !== commandName) continue;
124
+ if (!newest || (c.queuedTime ?? "") > (newest.queuedTime ?? "")) newest = c;
125
+ }
126
+ if (optimistic && !commands.some((c) => c.token === optimistic.token)) {
127
+ return { token: optimistic.token, value: optimistic.value, status: "QUEUED" };
128
+ }
129
+ if (!newest) return null;
130
+ return { token: newest.token, value: readValue(kind, newest.payload, parameter), status: newest.status };
131
+ }
132
+
133
+ // src/command-control.tsx
134
+ import { useCallback, useEffect, useRef, useState } from "react";
135
+ import { css, fontVar, spaceVar } from "@devicechain/widget-sdk";
136
+
137
+ // src/command-control-strings.ts
138
+ import { viewerLocale } from "@devicechain/widget-sdk";
139
+ var CONTROL_STRINGS = {
140
+ en: {
141
+ noCommand: "No command configured",
142
+ setParameter: "Set which parameter this control sends",
143
+ parameterMissing: (n) => `The command has no parameter \u201C${n}\u201D`,
144
+ wrongType: (n, e) => `Parameter \u201C${n}\u201D is not ${e}`,
145
+ fixedChoices: (n) => `Parameter \u201C${n}\u201D takes fixed choices, not a range`,
146
+ otherRequired: (ns) => `Not sent: the command also requires ${ns.map((n) => `\u201C${n}\u201D`).join(", ")}, which has no default value`,
147
+ badDefault: (ns) => `Not sent: the default value of ${ns.map((n) => `\u201C${n}\u201D`).join(", ")} is not valid for the command`,
148
+ setRange: "Set min and max",
149
+ badRange: "Max must be above min",
150
+ numberKind: "a number",
151
+ booleanKind: "a boolean",
152
+ lastSent: "Last sent",
153
+ nothingSent: "Nothing sent yet",
154
+ beyondHistory: "Not among the recent commands",
155
+ noValue: "no value",
156
+ unsent: "Not sent yet. Release or press Enter to send",
157
+ sliderHint: "Arrow keys choose a value; press Enter to send it, or Escape to discard it.",
158
+ pending: "Pending",
159
+ delivered: "Delivered",
160
+ failed: "Failed",
161
+ sending: "Sending\u2026",
162
+ on: "On",
163
+ off: "Off",
164
+ viewOnly: "View only",
165
+ noPermission: "You don\u2019t have permission to send commands.",
166
+ noSeam: "Commands aren\u2019t available in this view.",
167
+ noDevice: "No device is selected for this command.",
168
+ sendFailed: "Couldn\u2019t send the command. Try again."
169
+ },
170
+ es: {
171
+ noCommand: "No hay un comando configurado",
172
+ setParameter: "Defina qu\xE9 par\xE1metro env\xEDa este control",
173
+ parameterMissing: (n) => `El comando no tiene el par\xE1metro \u201C${n}\u201D`,
174
+ wrongType: (n, e) => `El par\xE1metro \u201C${n}\u201D no es ${e}`,
175
+ fixedChoices: (n) => `El par\xE1metro \u201C${n}\u201D admite opciones fijas, no un rango`,
176
+ otherRequired: (ns) => `No se env\xEDa: el comando tambi\xE9n exige ${ns.map((n) => `\u201C${n}\u201D`).join(", ")}, que no tiene valor por defecto`,
177
+ badDefault: (ns) => `No se env\xEDa: el valor por defecto de ${ns.map((n) => `\u201C${n}\u201D`).join(", ")} no es v\xE1lido para el comando`,
178
+ setRange: "Defina m\xEDn. y m\xE1x.",
179
+ badRange: "El m\xE1x. debe superar el m\xEDn.",
180
+ numberKind: "un n\xFAmero",
181
+ booleanKind: "un booleano",
182
+ lastSent: "\xDAltimo enviado",
183
+ nothingSent: "A\xFAn no se ha enviado nada",
184
+ beyondHistory: "No figura entre los comandos recientes",
185
+ noValue: "sin valor",
186
+ unsent: "A\xFAn no enviado. Suelte o pulse Intro para enviar",
187
+ sliderHint: "Las flechas eligen un valor; pulse Intro para enviarlo o Escape para descartarlo.",
188
+ pending: "Pendiente",
189
+ delivered: "Entregado",
190
+ failed: "Fallido",
191
+ sending: "Enviando\u2026",
192
+ on: "Encendido",
193
+ off: "Apagado",
194
+ viewOnly: "Solo lectura",
195
+ noPermission: "No tiene permiso para enviar comandos.",
196
+ noSeam: "Los comandos no est\xE1n disponibles en esta vista.",
197
+ noDevice: "No hay un dispositivo seleccionado para este comando.",
198
+ sendFailed: "No se pudo enviar el comando. Int\xE9ntelo de nuevo."
199
+ }
200
+ };
201
+ function controlStrings(locale = viewerLocale()) {
202
+ return locale?.toLowerCase().split("-")[0] === "es" ? CONTROL_STRINGS.es : CONTROL_STRINGS.en;
203
+ }
204
+
205
+ // src/command-control.tsx
206
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
207
+ var TOUCH_TARGET_PX = 44;
208
+ function refusalText(r, s, expect) {
209
+ switch (r.code) {
210
+ case "noCommand":
211
+ return s.noCommand;
212
+ case "setParameter":
213
+ return s.setParameter;
214
+ case "parameterMissing":
215
+ return s.parameterMissing(r.name);
216
+ case "wrongType":
217
+ return s.wrongType(r.name, expect === "number" ? s.numberKind : s.booleanKind);
218
+ case "fixedChoices":
219
+ return s.fixedChoices(r.name);
220
+ case "otherRequired":
221
+ return s.otherRequired(r.names);
222
+ case "badDefault":
223
+ return s.badDefault(r.names);
224
+ case "setRange":
225
+ return s.setRange;
226
+ case "badRange":
227
+ return s.badRange;
228
+ }
229
+ }
230
+ function useCommandSender(actions, deviceToken, commandName, resetKey) {
231
+ const strings = controlStrings();
232
+ const canWrite = actions?.can("command:write") ?? false;
233
+ const hasSeam = typeof actions?.sendCommand === "function";
234
+ const canSend = canWrite && hasSeam && !!deviceToken && !!commandName;
235
+ const inFlight = useRef(false);
236
+ const generation = useRef(0);
237
+ const [pending, setPending] = useState(false);
238
+ const [error, setError] = useState(null);
239
+ const [optimistic, setOptimistic] = useState(null);
240
+ useEffect(() => {
241
+ generation.current += 1;
242
+ inFlight.current = false;
243
+ setPending(false);
244
+ setError(null);
245
+ setOptimistic(null);
246
+ }, [resetKey]);
247
+ const send = useCallback(
248
+ (payload, value) => {
249
+ if (!canSend || inFlight.current || !actions?.sendCommand || !deviceToken || !commandName) return false;
250
+ const gen = generation.current;
251
+ inFlight.current = true;
252
+ setPending(true);
253
+ setError(null);
254
+ const settle = () => {
255
+ if (gen !== generation.current) return;
256
+ inFlight.current = false;
257
+ setPending(false);
258
+ };
259
+ let promise;
260
+ try {
261
+ promise = actions.sendCommand(deviceToken, commandName, payload);
262
+ } catch {
263
+ setError(strings.sendFailed);
264
+ settle();
265
+ return true;
266
+ }
267
+ promise.then((dispatch) => {
268
+ if (gen !== generation.current) return;
269
+ if (dispatch.status === "rejected") setError(dispatch.reason);
270
+ else setOptimistic({ token: dispatch.token, value });
271
+ }).catch(() => {
272
+ if (gen === generation.current) setError(strings.sendFailed);
273
+ }).finally(settle);
274
+ return true;
275
+ },
276
+ [actions, canSend, commandName, deviceToken, strings.sendFailed]
277
+ );
278
+ const readOnlyReason = !hasSeam ? strings.noSeam : !canWrite ? strings.noPermission : !deviceToken ? strings.noDevice : void 0;
279
+ return { strings, readOnlyReason, canSend, pending, error, optimistic, send };
280
+ }
281
+ var note = {
282
+ fontSize: fontVar("xs"),
283
+ lineHeight: 1.25,
284
+ color: css("muted-foreground"),
285
+ overflowWrap: "anywhere"
286
+ };
287
+ function ControlEmpty({ message }) {
288
+ return /* @__PURE__ */ jsx(
289
+ "div",
290
+ {
291
+ role: "alert",
292
+ "data-control-empty": "",
293
+ style: {
294
+ display: "flex",
295
+ alignItems: "center",
296
+ justifyContent: "center",
297
+ height: "100%",
298
+ textAlign: "center",
299
+ color: css("muted-foreground"),
300
+ fontSize: fontVar("sm"),
301
+ overflowWrap: "anywhere"
302
+ },
303
+ children: message
304
+ }
305
+ );
306
+ }
307
+ var readOnlyId = (captionId) => `${captionId}-ro`;
308
+ function ControlNotes({ sender, captionId }) {
309
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
310
+ sender.readOnlyReason ? /* @__PURE__ */ jsx("span", { id: readOnlyId(captionId), hidden: true, children: sender.readOnlyReason }) : null,
311
+ sender.readOnlyReason ? /* @__PURE__ */ jsx("div", { "data-read-only": "", title: sender.readOnlyReason, style: { ...note, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }, children: sender.strings.viewOnly }) : null,
312
+ sender.error ? /* @__PURE__ */ jsx("div", { role: "alert", style: { ...note, color: commandStatusColor("FAILED") }, children: sender.error }) : null
313
+ ] });
314
+ }
315
+ function LastSentCaption({
316
+ id,
317
+ last,
318
+ format,
319
+ strings,
320
+ override,
321
+ truncated = false
322
+ }) {
323
+ const group = last ? deliveryGroup(last.status) : void 0;
324
+ const word = group === "pending" ? strings.pending : group === "delivered" ? strings.delivered : group === "failed" ? strings.failed : last ? commandStatusLabel(last.status) : "";
325
+ return /* @__PURE__ */ jsx(
326
+ "div",
327
+ {
328
+ id,
329
+ role: "status",
330
+ "aria-live": "polite",
331
+ "data-last-sent": "",
332
+ "data-delivery": group,
333
+ style: { ...note, fontSize: fontVar("sm"), display: "flex", flexWrap: "wrap", alignItems: "center", columnGap: spaceVar(1) },
334
+ children: override ? /* @__PURE__ */ jsx("span", { children: override }) : last ? /* @__PURE__ */ jsxs(Fragment, { children: [
335
+ /* @__PURE__ */ jsxs("span", { children: [
336
+ strings.lastSent,
337
+ ":"
338
+ ] }),
339
+ /* @__PURE__ */ jsx("span", { style: { color: css("foreground"), fontWeight: 600 }, children: format(last.value) }),
340
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\xB7" }),
341
+ /* @__PURE__ */ jsxs(
342
+ "span",
343
+ {
344
+ title: last.status,
345
+ style: { display: "inline-flex", alignItems: "center", gap: 4, color: commandStatusColor(last.status), fontWeight: 600 },
346
+ children: [
347
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", style: { width: 7, height: 7, borderRadius: "50%", background: commandStatusColor(last.status) } }),
348
+ word
349
+ ]
350
+ }
351
+ )
352
+ ] }) : /* @__PURE__ */ jsx("span", { children: truncated ? strings.beyondHistory : strings.nothingSent })
353
+ }
354
+ );
355
+ }
356
+
357
+ // src/widgets/command-slider.tsx
358
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
359
+ var withUnit = (v, unit) => unit ? `${v} ${unit}` : String(v);
360
+ function CommandSlider({ widget, data, actions }) {
361
+ const commandName = optString(widget.options, "commandName");
362
+ const commandLabel = optString(widget.options, "commandLabel") ?? commandName;
363
+ const parameterSchema = optString(widget.options, "parameterSchema") ?? null;
364
+ const parameter = optString(widget.options, "parameter");
365
+ const min = optNumber(widget.options, "min");
366
+ const max = optNumber(widget.options, "max");
367
+ const step = optNumber(widget.options, "step");
368
+ const binding = useMemo(
369
+ () => bindControl("number", { commandName, parameterSchema, parameter, min, max, step }),
370
+ [commandName, parameterSchema, parameter, min, max, step]
371
+ );
372
+ const sender = useCommandSender(
373
+ actions,
374
+ data.deviceToken,
375
+ commandName,
376
+ `${commandName ?? ""}::${parameterSchema ?? ""}::${parameter ?? ""}`
377
+ );
378
+ const last = binding.ok && commandName ? lastSent("number", data.commands, commandName, binding.param.name, sender.optimistic) : null;
379
+ const [draft, setDraftState] = useState2(null);
380
+ const draftRef = useRef2(null);
381
+ const setDraft = (v) => {
382
+ draftRef.current = v;
383
+ setDraftState(v);
384
+ };
385
+ const captionId = useId();
386
+ const hintId = useId();
387
+ const pressed = useRef2(false);
388
+ const offRelease = useRef2(null);
389
+ useEffect2(() => () => offRelease.current?.(), []);
390
+ const discard = () => {
391
+ pressed.current = false;
392
+ offRelease.current?.();
393
+ offRelease.current = null;
394
+ if (draftRef.current !== null) setDraft(null);
395
+ };
396
+ const press = (input) => {
397
+ pressed.current = true;
398
+ offRelease.current?.();
399
+ const doc = input.ownerDocument;
400
+ const onRelease = (e) => {
401
+ if (e.target instanceof Node && input.contains(e.target)) return;
402
+ discard();
403
+ };
404
+ doc.addEventListener("pointerup", onRelease, true);
405
+ offRelease.current = () => doc.removeEventListener("pointerup", onRelease, true);
406
+ };
407
+ const commit = () => {
408
+ if (!binding.ok) return;
409
+ const staged = draftRef.current;
410
+ if (staged === null || !sender.canSend) return;
411
+ if (sender.send(payloadFor(binding, staged), staged)) setDraft(null);
412
+ };
413
+ const release = () => {
414
+ pressed.current = false;
415
+ offRelease.current?.();
416
+ offRelease.current = null;
417
+ commit();
418
+ };
419
+ return /* @__PURE__ */ jsx2(WidgetFrame, { title: optString(widget.options, "title"), children: (scale) => !binding.ok ? /* @__PURE__ */ jsx2(ControlEmpty, { message: refusalText(binding.refusal, sender.strings, "number") }) : /* @__PURE__ */ jsx2(
420
+ SliderBody,
421
+ {
422
+ binding,
423
+ label: commandLabel ?? binding.param.name,
424
+ draft,
425
+ lastValue: typeof last?.value === "number" ? last.value : void 0,
426
+ sender,
427
+ captionId,
428
+ hintId,
429
+ caption: /* @__PURE__ */ jsx2(
430
+ LastSentCaption,
431
+ {
432
+ id: captionId,
433
+ last,
434
+ strings: sender.strings,
435
+ truncated: data.total > data.commands.length,
436
+ format: (v) => typeof v === "number" ? withUnit(v, binding.param.unit) : sender.strings.noValue,
437
+ override: draft !== null ? sender.strings.unsent : sender.pending ? sender.strings.sending : void 0
438
+ }
439
+ ),
440
+ onStage: (v) => {
441
+ if (!sender.pending) setDraft(clampToRange(binding.range, v));
442
+ },
443
+ onCommit: commit,
444
+ onPress: press,
445
+ onRelease: release,
446
+ onDiscard: discard,
447
+ onLostCapture: () => {
448
+ if (pressed.current) discard();
449
+ },
450
+ scale
451
+ }
452
+ ) });
453
+ }
454
+ function SliderBody({
455
+ binding,
456
+ label,
457
+ draft,
458
+ lastValue,
459
+ sender,
460
+ captionId,
461
+ hintId,
462
+ caption,
463
+ onStage,
464
+ onCommit,
465
+ onPress,
466
+ onRelease,
467
+ onDiscard,
468
+ onLostCapture,
469
+ scale: ws
470
+ }) {
471
+ const range = binding.range;
472
+ const unit = binding.param.unit;
473
+ const disabled = !sender.canSend;
474
+ const busy = sender.pending;
475
+ const rest = lastValue !== void 0 ? clampToRange(range, lastValue) : range.min;
476
+ const shown = draft ?? rest;
477
+ const known = draft !== null || lastValue !== void 0;
478
+ const readout = known ? String(shown) : "\u2014";
479
+ const chars = useStickyChars(readout.length + (unit && known ? Math.ceil(unit.length / 2) + 1 : 0));
480
+ const floor = Math.round(FIT_MIN_FONT_PX * ws.scale);
481
+ const measured = ws.size.width > 0 && ws.size.height > 0;
482
+ const room = {
483
+ width: Math.floor(ws.size.width * 0.34),
484
+ height: Math.max(TOUCH_TARGET_PX, Math.floor(ws.size.height * 0.5))
485
+ };
486
+ const font = measured ? fitValueFont(room, chars, { floor }) : fitValueFontCss(chars, { floor });
487
+ return /* @__PURE__ */ jsxs2(
488
+ "div",
489
+ {
490
+ style: {
491
+ height: "100%",
492
+ boxSizing: "border-box",
493
+ display: "flex",
494
+ flexDirection: "column",
495
+ justifyContent: "safe center",
496
+ gap: spaceVar2(1),
497
+ overflow: "hidden"
498
+ },
499
+ children: [
500
+ /* @__PURE__ */ jsxs2("div", { style: { display: "flex", alignItems: "center", gap: spaceVar2(2), minHeight: TOUCH_TARGET_PX }, children: [
501
+ /* @__PURE__ */ jsx2(
502
+ "input",
503
+ {
504
+ type: "range",
505
+ "aria-label": label,
506
+ "aria-valuetext": withUnit(shown, unit),
507
+ "aria-describedby": `${hintId} ${captionId}${sender.readOnlyReason ? ` ${readOnlyId(captionId)}` : ""}`,
508
+ "aria-disabled": busy || void 0,
509
+ "aria-busy": busy || void 0,
510
+ min: range.min,
511
+ max: range.max,
512
+ step: range.step,
513
+ value: shown,
514
+ disabled,
515
+ "data-staged": draft !== null ? "true" : void 0,
516
+ onChange: (e) => onStage(Number(e.target.value)),
517
+ onPointerDown: (e) => onPress(e.currentTarget),
518
+ onPointerUp: onRelease,
519
+ onPointerCancel: onDiscard,
520
+ onLostPointerCapture: onLostCapture,
521
+ onBlur: onDiscard,
522
+ onKeyDown: (e) => {
523
+ if (e.key === "Enter") onCommit();
524
+ else if (e.key === "Escape") onDiscard();
525
+ },
526
+ style: {
527
+ flex: "1 1 0",
528
+ minWidth: 0,
529
+ height: TOUCH_TARGET_PX,
530
+ margin: 0,
531
+ accentColor: css2("primary"),
532
+ cursor: disabled || busy ? "not-allowed" : "pointer"
533
+ }
534
+ }
535
+ ),
536
+ /* @__PURE__ */ jsx2("span", { id: hintId, hidden: true, children: sender.strings.sliderHint }),
537
+ /* @__PURE__ */ jsxs2(
538
+ "output",
539
+ {
540
+ "aria-hidden": "true",
541
+ "data-slider-readout": "",
542
+ style: {
543
+ flex: "0 0 auto",
544
+ fontSize: font,
545
+ fontWeight: 600,
546
+ lineHeight: 1.1,
547
+ whiteSpace: "nowrap",
548
+ fontVariantNumeric: "tabular-nums",
549
+ color: draft !== null ? css2("foreground") : css2("muted-foreground")
550
+ },
551
+ children: [
552
+ readout,
553
+ unit && known ? /* @__PURE__ */ jsx2("span", { style: { fontSize: fontVar2("sm"), marginLeft: spaceVar2(1), color: css2("muted-foreground") }, children: unit }) : null
554
+ ]
555
+ }
556
+ )
557
+ ] }),
558
+ /* @__PURE__ */ jsxs2("div", { style: { display: "flex", flexWrap: "wrap", alignItems: "baseline", columnGap: spaceVar2(2) }, children: [
559
+ caption,
560
+ /* @__PURE__ */ jsx2(ControlNotes, { sender, captionId })
561
+ ] })
562
+ ]
563
+ }
564
+ );
565
+ }
566
+
567
+ // src/widgets/command-toggle.tsx
568
+ import { useId as useId2, useMemo as useMemo2 } from "react";
569
+ import {
570
+ css as css3,
571
+ FIT_MIN_FONT_PX as FIT_MIN_FONT_PX2,
572
+ fitValueFont as fitValueFont2,
573
+ fitValueFontCss as fitValueFontCss2,
574
+ optString as optString2,
575
+ spaceVar as spaceVar3,
576
+ useStickyChars as useStickyChars2,
577
+ WidgetFrame as WidgetFrame2
578
+ } from "@devicechain/widget-sdk";
579
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
580
+ var TRACK_W = 52;
581
+ var TRACK_H = 30;
582
+ var THUMB = 24;
583
+ function CommandToggle({ widget, data, actions }) {
584
+ const commandName = optString2(widget.options, "commandName");
585
+ const commandLabel = optString2(widget.options, "commandLabel") ?? commandName;
586
+ const parameterSchema = optString2(widget.options, "parameterSchema") ?? null;
587
+ const parameter = optString2(widget.options, "parameter");
588
+ const binding = useMemo2(
589
+ () => bindControl("boolean", { commandName, parameterSchema, parameter }),
590
+ [commandName, parameterSchema, parameter]
591
+ );
592
+ const sender = useCommandSender(
593
+ actions,
594
+ data.deviceToken,
595
+ commandName,
596
+ `${commandName ?? ""}::${parameterSchema ?? ""}::${parameter ?? ""}`
597
+ );
598
+ const last = binding.ok && commandName ? lastSent("boolean", data.commands, commandName, binding.param.name, sender.optimistic) : null;
599
+ const captionId = useId2();
600
+ return /* @__PURE__ */ jsx3(WidgetFrame2, { title: optString2(widget.options, "title"), children: (scale) => !binding.ok ? /* @__PURE__ */ jsx3(ControlEmpty, { message: refusalText(binding.refusal, sender.strings, "boolean") }) : /* @__PURE__ */ jsx3(
601
+ ToggleBody,
602
+ {
603
+ binding,
604
+ label: commandLabel ?? binding.param.name,
605
+ value: intentOf(last),
606
+ sender,
607
+ captionId,
608
+ caption: /* @__PURE__ */ jsx3(
609
+ LastSentCaption,
610
+ {
611
+ id: captionId,
612
+ last,
613
+ strings: sender.strings,
614
+ truncated: data.total > data.commands.length,
615
+ format: (v) => typeof v === "boolean" ? v ? sender.strings.on : sender.strings.off : sender.strings.noValue,
616
+ override: sender.pending ? sender.strings.sending : void 0
617
+ }
618
+ ),
619
+ scale
620
+ }
621
+ ) });
622
+ }
623
+ function intentOf(last) {
624
+ if (!last || typeof last.value !== "boolean") return void 0;
625
+ const group = deliveryGroup(last.status);
626
+ return group === "pending" || group === "delivered" ? last.value : void 0;
627
+ }
628
+ function ToggleBody({
629
+ binding,
630
+ label,
631
+ value,
632
+ sender,
633
+ captionId,
634
+ caption,
635
+ scale: ws
636
+ }) {
637
+ const checked = value === true;
638
+ const unknown = value === void 0;
639
+ const disabled = !sender.canSend;
640
+ const word = checked ? sender.strings.on : sender.strings.off;
641
+ const chars = useStickyChars2(word.length);
642
+ const floor = Math.round(FIT_MIN_FONT_PX2 * ws.scale);
643
+ const measured = ws.size.width > 0 && ws.size.height > 0;
644
+ const room = {
645
+ width: Math.floor(ws.size.width - TRACK_W - 16),
646
+ height: Math.max(TOUCH_TARGET_PX, Math.floor(ws.size.height * 0.5))
647
+ };
648
+ const font = measured ? fitValueFont2(room, chars, { floor }) : fitValueFontCss2(chars, { floor });
649
+ const sendValue = (next) => {
650
+ sender.send(payloadFor(binding, next), next);
651
+ };
652
+ return /* @__PURE__ */ jsxs3(
653
+ "div",
654
+ {
655
+ style: {
656
+ height: "100%",
657
+ boxSizing: "border-box",
658
+ display: "flex",
659
+ flexDirection: "column",
660
+ justifyContent: "safe center",
661
+ gap: spaceVar3(1),
662
+ overflow: "hidden"
663
+ },
664
+ children: [
665
+ unknown ? /* @__PURE__ */ jsx3("div", { role: "group", "aria-label": label, "aria-describedby": sender.readOnlyReason ? `${captionId} ${readOnlyId(captionId)}` : captionId, "data-toggle-choose": "", style: { display: "flex", gap: spaceVar3(2) }, children: [true, false].map((v) => /* @__PURE__ */ jsx3(
666
+ "button",
667
+ {
668
+ type: "button",
669
+ disabled,
670
+ "aria-busy": sender.pending,
671
+ onClick: () => sendValue(v),
672
+ style: {
673
+ flex: "1 1 0",
674
+ minHeight: TOUCH_TARGET_PX,
675
+ minWidth: TOUCH_TARGET_PX,
676
+ padding: `0 ${spaceVar3(2)}`,
677
+ borderRadius: 6,
678
+ border: `1px solid ${css3("border")}`,
679
+ background: css3("background"),
680
+ color: css3("foreground"),
681
+ fontWeight: 600,
682
+ cursor: disabled ? "not-allowed" : "pointer",
683
+ opacity: disabled ? 0.6 : 1
684
+ },
685
+ children: v ? sender.strings.on : sender.strings.off
686
+ },
687
+ String(v)
688
+ )) }) : /* @__PURE__ */ jsxs3(
689
+ "button",
690
+ {
691
+ type: "button",
692
+ role: "switch",
693
+ "aria-checked": checked,
694
+ "aria-label": label,
695
+ "aria-describedby": sender.readOnlyReason ? `${captionId} ${readOnlyId(captionId)}` : captionId,
696
+ "aria-busy": sender.pending,
697
+ disabled,
698
+ onClick: () => sendValue(!checked),
699
+ style: {
700
+ display: "flex",
701
+ alignItems: "center",
702
+ gap: spaceVar3(2),
703
+ minHeight: TOUCH_TARGET_PX,
704
+ minWidth: TOUCH_TARGET_PX,
705
+ padding: 0,
706
+ border: "none",
707
+ background: "transparent",
708
+ color: css3("foreground"),
709
+ cursor: disabled ? "not-allowed" : "pointer",
710
+ opacity: disabled ? 0.6 : 1,
711
+ textAlign: "left"
712
+ },
713
+ children: [
714
+ /* @__PURE__ */ jsx3(
715
+ "span",
716
+ {
717
+ "aria-hidden": "true",
718
+ style: {
719
+ position: "relative",
720
+ flex: "0 0 auto",
721
+ width: TRACK_W,
722
+ height: TRACK_H,
723
+ borderRadius: TRACK_H / 2,
724
+ boxSizing: "border-box",
725
+ border: `1px solid ${css3("border")}`,
726
+ background: checked ? css3("primary") : css3("muted")
727
+ },
728
+ children: /* @__PURE__ */ jsx3(
729
+ "span",
730
+ {
731
+ style: {
732
+ position: "absolute",
733
+ top: (TRACK_H - THUMB) / 2 - 1,
734
+ left: checked ? TRACK_W - THUMB - 3 : 2,
735
+ width: THUMB,
736
+ height: THUMB,
737
+ borderRadius: "50%",
738
+ background: checked ? css3("primary-foreground") : css3("background"),
739
+ border: `1px solid ${css3("border")}`,
740
+ boxSizing: "border-box"
741
+ }
742
+ }
743
+ )
744
+ }
745
+ ),
746
+ /* @__PURE__ */ jsx3(
747
+ "span",
748
+ {
749
+ "data-toggle-word": "",
750
+ style: {
751
+ fontSize: font,
752
+ fontWeight: 600,
753
+ lineHeight: 1.1,
754
+ whiteSpace: "nowrap",
755
+ color: css3("foreground")
756
+ },
757
+ children: word
758
+ }
759
+ )
760
+ ]
761
+ }
762
+ ),
763
+ /* @__PURE__ */ jsxs3("div", { style: { display: "flex", flexWrap: "wrap", alignItems: "baseline", columnGap: spaceVar3(2) }, children: [
764
+ caption,
765
+ /* @__PURE__ */ jsx3(ControlNotes, { sender, captionId })
766
+ ] })
767
+ ]
768
+ }
769
+ );
770
+ }
771
+ export {
772
+ CommandSlider,
773
+ CommandToggle
774
+ };
775
+ //# sourceMappingURL=control-widgets-UDS4ET3T.js.map