@camstack/ui-library 1.2.327 → 1.2.328

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.
@@ -0,0 +1,24 @@
1
+ import type { SensorDescriptor } from "./sensor-value-atom";
2
+ export type SnapshotStatus = "pending" | "error" | "success";
3
+ export type SensorHeroResolution = {
4
+ readonly kind: "value";
5
+ readonly slice: unknown;
6
+ } | {
7
+ readonly kind: "awaiting";
8
+ } | {
9
+ readonly kind: "quiet";
10
+ };
11
+ export interface SensorHeroSourceInput {
12
+ readonly addonId: string;
13
+ readonly role: string | null | undefined;
14
+ readonly descriptor: SensorDescriptor | undefined;
15
+ readonly capName: string | undefined;
16
+ readonly live: unknown;
17
+ readonly snapshotStatus: SnapshotStatus;
18
+ readonly snapshotCaps: unknown;
19
+ }
20
+ /** Cap a role's primary reading is stored on. Kept beside the hero, not a second renderer. */
21
+ export declare const ROLE_OUTPUT_CAP: Readonly<Record<string, string>>;
22
+ /** The descriptor can render this slice. `false` and `0` count; a missing field does not. */
23
+ export declare function sensorSliceReady(descriptor: SensorDescriptor, slice: unknown): boolean;
24
+ export declare function resolveSensorHeroReading(input: SensorHeroSourceInput): SensorHeroResolution;
package/dist/index.cjs CHANGED
@@ -14428,7 +14428,7 @@ Button.displayName = "Button";
14428
14428
  //#region src/composites/account-sign-in-field.tsx
14429
14429
  var INPUT_CLASS$1 = "w-full rounded-md border border-border bg-background px-2.5 py-1.5 text-xs text-foreground focus:border-primary focus:ring-1 focus:ring-primary/30 outline-none disabled:opacity-50 disabled:cursor-not-allowed";
14430
14430
  var BARE_CODE = /^[A-Za-z0-9._~-]{8,}$/;
14431
- function isRecord$3(value) {
14431
+ function isRecord$4(value) {
14432
14432
  return typeof value === "object" && value !== null && !Array.isArray(value);
14433
14433
  }
14434
14434
  function isPresent(value) {
@@ -14457,7 +14457,7 @@ function choicesOf(value) {
14457
14457
  if (!Array.isArray(value)) return [];
14458
14458
  const out = [];
14459
14459
  for (const row of value) {
14460
- if (!isRecord$3(row) || typeof row["value"] !== "string" || typeof row["label"] !== "string") continue;
14460
+ if (!isRecord$4(row) || typeof row["value"] !== "string" || typeof row["label"] !== "string") continue;
14461
14461
  out.push({
14462
14462
  value: row["value"],
14463
14463
  label: row["label"],
@@ -14511,7 +14511,7 @@ function AccountSignInField({ field, values, disabled, onAction, onChangeMany })
14511
14511
  setError(null);
14512
14512
  try {
14513
14513
  const result = await onAction(field.beginAction, field.key, {});
14514
- if (!isRecord$3(result) || typeof result["authorizeUrl"] !== "string" || typeof result["attemptId"] !== "string") {
14514
+ if (!isRecord$4(result) || typeof result["authorizeUrl"] !== "string" || typeof result["attemptId"] !== "string") {
14515
14515
  setError(t("uiLibrary.accountSignIn.noLink", "The sign-in page could not be opened. Try again."));
14516
14516
  return;
14517
14517
  }
@@ -14539,7 +14539,7 @@ function AccountSignInField({ field, values, disabled, onAction, onChangeMany })
14539
14539
  attemptId,
14540
14540
  callback
14541
14541
  });
14542
- if (!isRecord$3(result)) {
14542
+ if (!isRecord$4(result)) {
14543
14543
  setError(t("uiLibrary.accountSignIn.rejected", "The sign-in was not accepted. Start again."));
14544
14544
  return;
14545
14545
  }
@@ -19906,11 +19906,11 @@ function useDeviceSnapshotImage(deviceId, options = {}) {
19906
19906
  //#region src/composites/cap-settings/camera-grid-layout.ts
19907
19907
  /** Smallest rectangle the editor will produce — ffmpeg refuses a zero-area crop. */
19908
19908
  var MIN_EXTENT = .02;
19909
- function isRecord$2(value) {
19909
+ function isRecord$3(value) {
19910
19910
  return value !== null && typeof value === "object" && !Array.isArray(value);
19911
19911
  }
19912
19912
  function readRect(value) {
19913
- if (!isRecord$2(value)) return null;
19913
+ if (!isRecord$3(value)) return null;
19914
19914
  const { x, y, width, height } = value;
19915
19915
  if (typeof x !== "number" || typeof y !== "number" || typeof width !== "number" || typeof height !== "number") return null;
19916
19916
  if (x < 0 || y < 0 || width <= 0 || height <= 0) return null;
@@ -19923,7 +19923,7 @@ function readRect(value) {
19923
19923
  };
19924
19924
  }
19925
19925
  function readCell(value) {
19926
- if (!isRecord$2(value)) return null;
19926
+ if (!isRecord$3(value)) return null;
19927
19927
  const deviceId = value["deviceId"];
19928
19928
  if (typeof deviceId !== "number" || !Number.isInteger(deviceId)) return null;
19929
19929
  const source = readRect(value["source"]);
@@ -19936,7 +19936,7 @@ function readCell(value) {
19936
19936
  };
19937
19937
  }
19938
19938
  function readProfileOffer(value) {
19939
- if (!isRecord$2(value)) return null;
19939
+ if (!isRecord$3(value)) return null;
19940
19940
  const profile = value["profile"];
19941
19941
  const offered = value["offered"];
19942
19942
  const missing = value["missingSources"];
@@ -19965,7 +19965,7 @@ function readProfileOffer(value) {
19965
19965
  */
19966
19966
  function parseGridView(raw) {
19967
19967
  if (raw === null) return null;
19968
- if (!isRecord$2(raw)) return "unreadable";
19968
+ if (!isRecord$3(raw)) return "unreadable";
19969
19969
  const { instanceId, deviceId, name, fps, cells, profiles } = raw;
19970
19970
  if (typeof instanceId !== "string" || typeof deviceId !== "number" || typeof name !== "string" || typeof fps !== "number" || !Array.isArray(cells) || !Array.isArray(profiles)) return "unreadable";
19971
19971
  const parsed = [];
@@ -34127,10 +34127,7 @@ function RunningBadge({ running, children, className }) {
34127
34127
  function RunningStatusLine({ running, since, phase, className, now = Date.now() }) {
34128
34128
  const t = useUiText();
34129
34129
  const parts = runningStatusParts(running, since, phase, now);
34130
- if (parts.key === "unknown") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
34131
- className: cn("text-[11px] text-foreground-subtle", className),
34132
- children: t("uiLibrary.runningStatus.unknown", "Unknown")
34133
- });
34130
+ if (parts.key === "unknown" || running !== true && running !== false) return null;
34134
34131
  if (parts.key === "idle") {
34135
34132
  const base = t("uiLibrary.runningStatus.idle", "Idle");
34136
34133
  const text = parts.phase === void 0 ? base : t("uiLibrary.runningStatus.withPhase", "{{state}} · {{phase}}", {
@@ -47382,6 +47379,92 @@ function SensorInlineControl({ trpc, deviceId }) {
47382
47379
  });
47383
47380
  }
47384
47381
  //#endregion
47382
+ //#region src/composites/device-controls/sensor-hero-source.ts
47383
+ /**
47384
+ * Where a sensor hero reads its number. The live proxy slice is the
47385
+ * usual source. A composed device publishes the same reading on the
47386
+ * snapshot before that proxy has a binding, and it has no running-state
47387
+ * to stand in for the reading. An answered snapshot with no primary
47388
+ * field is quiet — it must not say the value is still on its way.
47389
+ */
47390
+ /** Cap a role's primary reading is stored on. Kept beside the hero, not a second renderer. */
47391
+ var ROLE_OUTPUT_CAP = {
47392
+ "temperature-sensor": "temperature-sensor",
47393
+ "humidity-sensor": "humidity-sensor",
47394
+ "ambient-light-sensor": "ambient-light-sensor",
47395
+ "pressure-sensor": "pressure-sensor",
47396
+ "numeric-sensor": "numeric-sensor",
47397
+ "wind-sensor": "numeric-sensor",
47398
+ "rain-sensor": "numeric-sensor",
47399
+ "uv-sensor": "numeric-sensor",
47400
+ "solar-radiation-sensor": "numeric-sensor",
47401
+ "soil-moisture-sensor": "numeric-sensor",
47402
+ "power-sensor": "power-meter",
47403
+ "energy-sensor": "power-meter",
47404
+ "voltage-sensor": "power-meter",
47405
+ "current-sensor": "power-meter",
47406
+ "battery-sensor": "numeric-sensor",
47407
+ "generic-sensor": "numeric-sensor",
47408
+ "motion-sensor": "motion",
47409
+ "pir-sensor": "motion",
47410
+ "contact-sensor": "contact",
47411
+ "smoke-sensor": "smoke",
47412
+ "leak-sensor": "flood",
47413
+ "co-sensor": "carbon-monoxide",
47414
+ "binary-sensor": "binary",
47415
+ presence: "presence",
47416
+ "enum-sensor": "enum-sensor",
47417
+ "datetime-sensor": "enum-sensor"
47418
+ };
47419
+ function isRecord$2(value) {
47420
+ return typeof value === "object" && value !== null && !Array.isArray(value);
47421
+ }
47422
+ /** The descriptor can render this slice. `false` and `0` count; a missing field does not. */
47423
+ function sensorSliceReady(descriptor, slice) {
47424
+ if (!isRecord$2(slice)) return false;
47425
+ if (descriptor.kind === "presence") return typeof slice["state"] === "string";
47426
+ if (descriptor.kind === "string" || descriptor.kind === "datetime") return typeof slice["value"] === "string" && slice["value"] !== "";
47427
+ const value = slice[descriptor.field];
47428
+ if (descriptor.kind === "numeric") return typeof value === "number";
47429
+ return typeof value === "boolean";
47430
+ }
47431
+ function capSlice(caps, capName) {
47432
+ if (capName === void 0 || !isRecord$2(caps)) return void 0;
47433
+ return caps[capName];
47434
+ }
47435
+ /**
47436
+ * A battery group writes `battery.percentage`. The role's usual cap is
47437
+ * `numeric-sensor.value`. Hand the renderer a slice whose `value` is that
47438
+ * percentage so one numeric path draws both.
47439
+ */
47440
+ function batteryPercentageSlice(caps) {
47441
+ const battery = capSlice(caps, "battery");
47442
+ if (!isRecord$2(battery) || typeof battery["percentage"] !== "number") return void 0;
47443
+ return {
47444
+ ...battery,
47445
+ value: battery["percentage"]
47446
+ };
47447
+ }
47448
+ function snapshotSlice(input) {
47449
+ const direct = capSlice(input.snapshotCaps, input.capName);
47450
+ if (input.descriptor !== void 0 && sensorSliceReady(input.descriptor, direct)) return direct;
47451
+ if (input.role === "battery-sensor" && input.descriptor?.kind === "numeric") return batteryPercentageSlice(input.snapshotCaps);
47452
+ }
47453
+ function resolveSensorHeroReading(input) {
47454
+ const descriptor = input.descriptor;
47455
+ if (descriptor !== void 0 && sensorSliceReady(descriptor, input.live)) return {
47456
+ kind: "value",
47457
+ slice: input.live
47458
+ };
47459
+ const fromSnapshot = snapshotSlice(input);
47460
+ if (descriptor !== void 0 && sensorSliceReady(descriptor, fromSnapshot)) return {
47461
+ kind: "value",
47462
+ slice: fromSnapshot
47463
+ };
47464
+ if (input.addonId === _camstack_types.COMPOSER_ADDON_ID && input.snapshotStatus === "success") return { kind: "quiet" };
47465
+ return { kind: "awaiting" };
47466
+ }
47467
+ //#endregion
47385
47468
  //#region src/composites/device-controls/sensor-hero-card.tsx
47386
47469
  /**
47387
47470
  * SensorHeroCard — large-format sensor value display for the device-detail
@@ -47448,7 +47531,28 @@ function SensorHeroInner({ trpc, device }) {
47448
47531
  const occupancySlice = useDeviceStateSlice(dev !== null && !isBattery ? dev.state.binary : void 0);
47449
47532
  const useBinaryBattery = isBattery && roleDescriptor?.kind === "numeric" && !hasBatteryNumericValue(numericSlice, roleDescriptor.field) && binaryBatterySlice !== void 0;
47450
47533
  const descriptor = useBinaryBattery ? BINARY_BATTERY_DESCRIPTOR : roleDescriptor;
47451
- const slice = useBinaryBattery ? binaryBatterySlice : numericSlice;
47534
+ const liveSlice = useBinaryBattery ? binaryBatterySlice : numericSlice;
47535
+ const snapshot = useDeviceStateGetSnapshot({ deviceId: device.id }, { staleTime: 5e3 });
47536
+ const capName = useBinaryBattery ? "binary" : device.role != null ? ROLE_OUTPUT_CAP[device.role] : ROLE_OUTPUT_CAP["temperature-sensor"];
47537
+ const snapshotStatus = snapshot.status === "success" || snapshot.status === "error" ? snapshot.status : "pending";
47538
+ const resolution = resolveSensorHeroReading({
47539
+ addonId: device.addonId,
47540
+ role: device.role,
47541
+ descriptor,
47542
+ capName,
47543
+ live: liveSlice,
47544
+ snapshotStatus,
47545
+ snapshotCaps: snapshot.data
47546
+ });
47547
+ if (resolution.kind === "quiet") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
47548
+ className: "flex flex-col items-center justify-center gap-2 rounded-lg border border-border bg-surface p-4 min-h-[96px]",
47549
+ "data-testid": "sensor-hero-quiet",
47550
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
47551
+ className: "text-sm text-foreground-subtle",
47552
+ children: device.name
47553
+ })
47554
+ });
47555
+ const slice = resolution.kind === "value" ? resolution.slice : void 0;
47452
47556
  if (descriptor === void 0 || slice === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
47453
47557
  className: "flex flex-col items-center justify-center gap-2 rounded-lg border border-border bg-surface p-4 min-h-[96px]",
47454
47558
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -47467,7 +47571,7 @@ function SensorHeroInner({ trpc, device }) {
47467
47571
  const rendered = resolveSensorDisplay({
47468
47572
  display: device.display,
47469
47573
  capName: device.role === "numeric-sensor" ? "numeric-sensor" : void 0,
47470
- roleDefault: device.role != null ? roleDefaultsData?.defaults[device.role] : roleDefaultsData?.defaults["temperature-sensor"],
47574
+ roleDefault: device.role != null ? roleDefaultsData?.defaults?.[device.role] : roleDefaultsData?.defaults?.["temperature-sensor"],
47471
47575
  quantity: descriptor.quantity ?? "device",
47472
47576
  deviceQuantity: deviceQuantity === "device" ? void 0 : deviceQuantity
47473
47577
  }).format(rawValue);
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import * as __mfHostReact from "react";
5
5
  import React, { Fragment, Suspense, cloneElement, createContext, createElement, forwardRef, lazy, useCallback, useContext, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useReducer, useRef, useState, useSyncExternalStore } from "react";
6
6
  import * as __mfHostReactJsxRuntime from "react/jsx-runtime";
7
7
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
8
- import { ALL_CAPABILITY_DEFINITIONS, CAM_PROFILE_ORDER, CLIP_STILL_DISPOSITION_HEADER, CLIP_STILL_REASON_CODE_HEADER, CLIP_STILL_REASON_HEADER, CONTAINER_CHILD_PRIORITY, ConsumableLevelSchema, DEFAULT_DISPLAY_PREFERENCE, DEFAULT_EVENTS_BAND_BUFFER_SEC, DEVICE_HISTORY_ENERGY_KEY, DEVICE_HISTORY_QUERY_MANY_MAX_ITEMS, DEVICE_TYPE_CONTROL_KIND, DeviceExportStatusSchema, DeviceFeature, DeviceRole, DeviceType, EXTENSION_SLOTS, EXTENSION_SLOT_IDS, EventCategory, ExposedDeviceSchema, FIRST_LEVEL_MACRO_CLASSES, HISTORY_CATALOG, ICON_CATEGORIES, KNOWN_CAP_NAMES, MAX_SENSOR_TRIGGER_DEVICES, MODEL_PROVIDER_IDS, NAVIGATION_LIGHT_LEVEL_MAX, NAVIGATION_LIGHT_LEVEL_MIN, NODE_COLOR_IDS, NodeColorIdSchema, PALETTE_ENTRY_IDS, PET_FEEDER_MANUAL_FEED_MAX, PET_FEEDER_MANUAL_FEED_MIN, RECORDING_AUDIO_TRIGGER_OFFERED_DBFS, RECORDING_SOURCE_CAMSTACK_ADDON, REDACTED_SECRET, RecordingConfigSchema, RecordingStatusSchema, SECRET_CLEAR, SOURCE_CAP_ACTIVE_FIELD, STATIONARY_OVERLAY_STYLE, SUBJECT_PATH_DWELL_EPS, SUBJECT_PATH_FULL_FRAME_COVERAGE, SUBJECT_PATH_MAX_POINTS, TIMEZONES, USE_ROOT_MODEL_SETTING_KEY, assignNodeColorId, assignNodeColorId as assignNodeColorId$1, baseUnitOf, buildModelVariantGroups, buildSubjectPath, bundledCamstack, canConvertUnit, canonicalIconKey, clearModelBoundOverrides, clearModelBoundSetting, clusterStepSettingFieldsFor, collectModelBoundKeys, consumableFormOf, consumableKindOf, createDeviceProxy, createLazyTrpcSource, createSliceHandle, dehydrateSchema, deriveRecordingMode, describeModelVariant, displayUnitFor, effectiveStepSettings, findTimezone, formatParkedFor, formatReading, hasModelBoundOverrides, iconCategoryLabel, iconsInCategory, isDrawableSubjectPath, isKeychainStoredField, isModelScopedStepSettingKey, isReadingQuantity, isRootSuppliableStepId, isSecretClear, isSecretFieldView, isSecretMarker, modelBindingKey, normalizeUnit, paintHex, parseOccupancyScope, projectSubjectPath, publicEventCategories, resolveContainerPrimaryChild, resolveDeviceControlKind, resolveIconKey, resolveModelBoundSetting, resolveRecordingProfiles, resolveRootSupply, resolveVariantModelId, resolveViewableDeviceIds, searchIcons, selectAssignedProfileSlots, stepSettingSource, subjectPathSampleFromBox, subjectPathSvgPoints, tryConvertUnit, unitsForDimension, withFieldFrom, withoutField, writeModelBoundSetting } from "@camstack/types";
8
+ import { ALL_CAPABILITY_DEFINITIONS, CAM_PROFILE_ORDER, CLIP_STILL_DISPOSITION_HEADER, CLIP_STILL_REASON_CODE_HEADER, CLIP_STILL_REASON_HEADER, COMPOSER_ADDON_ID, CONTAINER_CHILD_PRIORITY, ConsumableLevelSchema, DEFAULT_DISPLAY_PREFERENCE, DEFAULT_EVENTS_BAND_BUFFER_SEC, DEVICE_HISTORY_ENERGY_KEY, DEVICE_HISTORY_QUERY_MANY_MAX_ITEMS, DEVICE_TYPE_CONTROL_KIND, DeviceExportStatusSchema, DeviceFeature, DeviceRole, DeviceType, EXTENSION_SLOTS, EXTENSION_SLOT_IDS, EventCategory, ExposedDeviceSchema, FIRST_LEVEL_MACRO_CLASSES, HISTORY_CATALOG, ICON_CATEGORIES, KNOWN_CAP_NAMES, MAX_SENSOR_TRIGGER_DEVICES, MODEL_PROVIDER_IDS, NAVIGATION_LIGHT_LEVEL_MAX, NAVIGATION_LIGHT_LEVEL_MIN, NODE_COLOR_IDS, NodeColorIdSchema, PALETTE_ENTRY_IDS, PET_FEEDER_MANUAL_FEED_MAX, PET_FEEDER_MANUAL_FEED_MIN, RECORDING_AUDIO_TRIGGER_OFFERED_DBFS, RECORDING_SOURCE_CAMSTACK_ADDON, REDACTED_SECRET, RecordingConfigSchema, RecordingStatusSchema, SECRET_CLEAR, SOURCE_CAP_ACTIVE_FIELD, STATIONARY_OVERLAY_STYLE, SUBJECT_PATH_DWELL_EPS, SUBJECT_PATH_FULL_FRAME_COVERAGE, SUBJECT_PATH_MAX_POINTS, TIMEZONES, USE_ROOT_MODEL_SETTING_KEY, assignNodeColorId, assignNodeColorId as assignNodeColorId$1, baseUnitOf, buildModelVariantGroups, buildSubjectPath, bundledCamstack, canConvertUnit, canonicalIconKey, clearModelBoundOverrides, clearModelBoundSetting, clusterStepSettingFieldsFor, collectModelBoundKeys, consumableFormOf, consumableKindOf, createDeviceProxy, createLazyTrpcSource, createSliceHandle, dehydrateSchema, deriveRecordingMode, describeModelVariant, displayUnitFor, effectiveStepSettings, findTimezone, formatParkedFor, formatReading, hasModelBoundOverrides, iconCategoryLabel, iconsInCategory, isDrawableSubjectPath, isKeychainStoredField, isModelScopedStepSettingKey, isReadingQuantity, isRootSuppliableStepId, isSecretClear, isSecretFieldView, isSecretMarker, modelBindingKey, normalizeUnit, paintHex, parseOccupancyScope, projectSubjectPath, publicEventCategories, resolveContainerPrimaryChild, resolveDeviceControlKind, resolveIconKey, resolveModelBoundSetting, resolveRecordingProfiles, resolveRootSupply, resolveVariantModelId, resolveViewableDeviceIds, searchIcons, selectAssignedProfileSlots, stepSettingSource, subjectPathSampleFromBox, subjectPathSvgPoints, tryConvertUnit, unitsForDimension, withFieldFrom, withoutField, writeModelBoundSetting } from "@camstack/types";
9
9
  import * as __mfHostTrpcReactQuery from "@trpc/react-query";
10
10
  import { createTRPCReact } from "@trpc/react-query";
11
11
  import * as __mfHostReactDOM from "react-dom";
@@ -14404,7 +14404,7 @@ Button.displayName = "Button";
14404
14404
  //#region src/composites/account-sign-in-field.tsx
14405
14405
  var INPUT_CLASS$1 = "w-full rounded-md border border-border bg-background px-2.5 py-1.5 text-xs text-foreground focus:border-primary focus:ring-1 focus:ring-primary/30 outline-none disabled:opacity-50 disabled:cursor-not-allowed";
14406
14406
  var BARE_CODE = /^[A-Za-z0-9._~-]{8,}$/;
14407
- function isRecord$3(value) {
14407
+ function isRecord$4(value) {
14408
14408
  return typeof value === "object" && value !== null && !Array.isArray(value);
14409
14409
  }
14410
14410
  function isPresent(value) {
@@ -14433,7 +14433,7 @@ function choicesOf(value) {
14433
14433
  if (!Array.isArray(value)) return [];
14434
14434
  const out = [];
14435
14435
  for (const row of value) {
14436
- if (!isRecord$3(row) || typeof row["value"] !== "string" || typeof row["label"] !== "string") continue;
14436
+ if (!isRecord$4(row) || typeof row["value"] !== "string" || typeof row["label"] !== "string") continue;
14437
14437
  out.push({
14438
14438
  value: row["value"],
14439
14439
  label: row["label"],
@@ -14487,7 +14487,7 @@ function AccountSignInField({ field, values, disabled, onAction, onChangeMany })
14487
14487
  setError(null);
14488
14488
  try {
14489
14489
  const result = await onAction(field.beginAction, field.key, {});
14490
- if (!isRecord$3(result) || typeof result["authorizeUrl"] !== "string" || typeof result["attemptId"] !== "string") {
14490
+ if (!isRecord$4(result) || typeof result["authorizeUrl"] !== "string" || typeof result["attemptId"] !== "string") {
14491
14491
  setError(t("uiLibrary.accountSignIn.noLink", "The sign-in page could not be opened. Try again."));
14492
14492
  return;
14493
14493
  }
@@ -14515,7 +14515,7 @@ function AccountSignInField({ field, values, disabled, onAction, onChangeMany })
14515
14515
  attemptId,
14516
14516
  callback
14517
14517
  });
14518
- if (!isRecord$3(result)) {
14518
+ if (!isRecord$4(result)) {
14519
14519
  setError(t("uiLibrary.accountSignIn.rejected", "The sign-in was not accepted. Start again."));
14520
14520
  return;
14521
14521
  }
@@ -19882,11 +19882,11 @@ function useDeviceSnapshotImage(deviceId, options = {}) {
19882
19882
  //#region src/composites/cap-settings/camera-grid-layout.ts
19883
19883
  /** Smallest rectangle the editor will produce — ffmpeg refuses a zero-area crop. */
19884
19884
  var MIN_EXTENT = .02;
19885
- function isRecord$2(value) {
19885
+ function isRecord$3(value) {
19886
19886
  return value !== null && typeof value === "object" && !Array.isArray(value);
19887
19887
  }
19888
19888
  function readRect(value) {
19889
- if (!isRecord$2(value)) return null;
19889
+ if (!isRecord$3(value)) return null;
19890
19890
  const { x, y, width, height } = value;
19891
19891
  if (typeof x !== "number" || typeof y !== "number" || typeof width !== "number" || typeof height !== "number") return null;
19892
19892
  if (x < 0 || y < 0 || width <= 0 || height <= 0) return null;
@@ -19899,7 +19899,7 @@ function readRect(value) {
19899
19899
  };
19900
19900
  }
19901
19901
  function readCell(value) {
19902
- if (!isRecord$2(value)) return null;
19902
+ if (!isRecord$3(value)) return null;
19903
19903
  const deviceId = value["deviceId"];
19904
19904
  if (typeof deviceId !== "number" || !Number.isInteger(deviceId)) return null;
19905
19905
  const source = readRect(value["source"]);
@@ -19912,7 +19912,7 @@ function readCell(value) {
19912
19912
  };
19913
19913
  }
19914
19914
  function readProfileOffer(value) {
19915
- if (!isRecord$2(value)) return null;
19915
+ if (!isRecord$3(value)) return null;
19916
19916
  const profile = value["profile"];
19917
19917
  const offered = value["offered"];
19918
19918
  const missing = value["missingSources"];
@@ -19941,7 +19941,7 @@ function readProfileOffer(value) {
19941
19941
  */
19942
19942
  function parseGridView(raw) {
19943
19943
  if (raw === null) return null;
19944
- if (!isRecord$2(raw)) return "unreadable";
19944
+ if (!isRecord$3(raw)) return "unreadable";
19945
19945
  const { instanceId, deviceId, name, fps, cells, profiles } = raw;
19946
19946
  if (typeof instanceId !== "string" || typeof deviceId !== "number" || typeof name !== "string" || typeof fps !== "number" || !Array.isArray(cells) || !Array.isArray(profiles)) return "unreadable";
19947
19947
  const parsed = [];
@@ -34103,10 +34103,7 @@ function RunningBadge({ running, children, className }) {
34103
34103
  function RunningStatusLine({ running, since, phase, className, now = Date.now() }) {
34104
34104
  const t = useUiText();
34105
34105
  const parts = runningStatusParts(running, since, phase, now);
34106
- if (parts.key === "unknown") return /* @__PURE__ */ jsx("p", {
34107
- className: cn("text-[11px] text-foreground-subtle", className),
34108
- children: t("uiLibrary.runningStatus.unknown", "Unknown")
34109
- });
34106
+ if (parts.key === "unknown" || running !== true && running !== false) return null;
34110
34107
  if (parts.key === "idle") {
34111
34108
  const base = t("uiLibrary.runningStatus.idle", "Idle");
34112
34109
  const text = parts.phase === void 0 ? base : t("uiLibrary.runningStatus.withPhase", "{{state}} · {{phase}}", {
@@ -47358,6 +47355,92 @@ function SensorInlineControl({ trpc, deviceId }) {
47358
47355
  });
47359
47356
  }
47360
47357
  //#endregion
47358
+ //#region src/composites/device-controls/sensor-hero-source.ts
47359
+ /**
47360
+ * Where a sensor hero reads its number. The live proxy slice is the
47361
+ * usual source. A composed device publishes the same reading on the
47362
+ * snapshot before that proxy has a binding, and it has no running-state
47363
+ * to stand in for the reading. An answered snapshot with no primary
47364
+ * field is quiet — it must not say the value is still on its way.
47365
+ */
47366
+ /** Cap a role's primary reading is stored on. Kept beside the hero, not a second renderer. */
47367
+ var ROLE_OUTPUT_CAP = {
47368
+ "temperature-sensor": "temperature-sensor",
47369
+ "humidity-sensor": "humidity-sensor",
47370
+ "ambient-light-sensor": "ambient-light-sensor",
47371
+ "pressure-sensor": "pressure-sensor",
47372
+ "numeric-sensor": "numeric-sensor",
47373
+ "wind-sensor": "numeric-sensor",
47374
+ "rain-sensor": "numeric-sensor",
47375
+ "uv-sensor": "numeric-sensor",
47376
+ "solar-radiation-sensor": "numeric-sensor",
47377
+ "soil-moisture-sensor": "numeric-sensor",
47378
+ "power-sensor": "power-meter",
47379
+ "energy-sensor": "power-meter",
47380
+ "voltage-sensor": "power-meter",
47381
+ "current-sensor": "power-meter",
47382
+ "battery-sensor": "numeric-sensor",
47383
+ "generic-sensor": "numeric-sensor",
47384
+ "motion-sensor": "motion",
47385
+ "pir-sensor": "motion",
47386
+ "contact-sensor": "contact",
47387
+ "smoke-sensor": "smoke",
47388
+ "leak-sensor": "flood",
47389
+ "co-sensor": "carbon-monoxide",
47390
+ "binary-sensor": "binary",
47391
+ presence: "presence",
47392
+ "enum-sensor": "enum-sensor",
47393
+ "datetime-sensor": "enum-sensor"
47394
+ };
47395
+ function isRecord$2(value) {
47396
+ return typeof value === "object" && value !== null && !Array.isArray(value);
47397
+ }
47398
+ /** The descriptor can render this slice. `false` and `0` count; a missing field does not. */
47399
+ function sensorSliceReady(descriptor, slice) {
47400
+ if (!isRecord$2(slice)) return false;
47401
+ if (descriptor.kind === "presence") return typeof slice["state"] === "string";
47402
+ if (descriptor.kind === "string" || descriptor.kind === "datetime") return typeof slice["value"] === "string" && slice["value"] !== "";
47403
+ const value = slice[descriptor.field];
47404
+ if (descriptor.kind === "numeric") return typeof value === "number";
47405
+ return typeof value === "boolean";
47406
+ }
47407
+ function capSlice(caps, capName) {
47408
+ if (capName === void 0 || !isRecord$2(caps)) return void 0;
47409
+ return caps[capName];
47410
+ }
47411
+ /**
47412
+ * A battery group writes `battery.percentage`. The role's usual cap is
47413
+ * `numeric-sensor.value`. Hand the renderer a slice whose `value` is that
47414
+ * percentage so one numeric path draws both.
47415
+ */
47416
+ function batteryPercentageSlice(caps) {
47417
+ const battery = capSlice(caps, "battery");
47418
+ if (!isRecord$2(battery) || typeof battery["percentage"] !== "number") return void 0;
47419
+ return {
47420
+ ...battery,
47421
+ value: battery["percentage"]
47422
+ };
47423
+ }
47424
+ function snapshotSlice(input) {
47425
+ const direct = capSlice(input.snapshotCaps, input.capName);
47426
+ if (input.descriptor !== void 0 && sensorSliceReady(input.descriptor, direct)) return direct;
47427
+ if (input.role === "battery-sensor" && input.descriptor?.kind === "numeric") return batteryPercentageSlice(input.snapshotCaps);
47428
+ }
47429
+ function resolveSensorHeroReading(input) {
47430
+ const descriptor = input.descriptor;
47431
+ if (descriptor !== void 0 && sensorSliceReady(descriptor, input.live)) return {
47432
+ kind: "value",
47433
+ slice: input.live
47434
+ };
47435
+ const fromSnapshot = snapshotSlice(input);
47436
+ if (descriptor !== void 0 && sensorSliceReady(descriptor, fromSnapshot)) return {
47437
+ kind: "value",
47438
+ slice: fromSnapshot
47439
+ };
47440
+ if (input.addonId === COMPOSER_ADDON_ID && input.snapshotStatus === "success") return { kind: "quiet" };
47441
+ return { kind: "awaiting" };
47442
+ }
47443
+ //#endregion
47361
47444
  //#region src/composites/device-controls/sensor-hero-card.tsx
47362
47445
  /**
47363
47446
  * SensorHeroCard — large-format sensor value display for the device-detail
@@ -47424,7 +47507,28 @@ function SensorHeroInner({ trpc, device }) {
47424
47507
  const occupancySlice = useDeviceStateSlice(dev !== null && !isBattery ? dev.state.binary : void 0);
47425
47508
  const useBinaryBattery = isBattery && roleDescriptor?.kind === "numeric" && !hasBatteryNumericValue(numericSlice, roleDescriptor.field) && binaryBatterySlice !== void 0;
47426
47509
  const descriptor = useBinaryBattery ? BINARY_BATTERY_DESCRIPTOR : roleDescriptor;
47427
- const slice = useBinaryBattery ? binaryBatterySlice : numericSlice;
47510
+ const liveSlice = useBinaryBattery ? binaryBatterySlice : numericSlice;
47511
+ const snapshot = useDeviceStateGetSnapshot({ deviceId: device.id }, { staleTime: 5e3 });
47512
+ const capName = useBinaryBattery ? "binary" : device.role != null ? ROLE_OUTPUT_CAP[device.role] : ROLE_OUTPUT_CAP["temperature-sensor"];
47513
+ const snapshotStatus = snapshot.status === "success" || snapshot.status === "error" ? snapshot.status : "pending";
47514
+ const resolution = resolveSensorHeroReading({
47515
+ addonId: device.addonId,
47516
+ role: device.role,
47517
+ descriptor,
47518
+ capName,
47519
+ live: liveSlice,
47520
+ snapshotStatus,
47521
+ snapshotCaps: snapshot.data
47522
+ });
47523
+ if (resolution.kind === "quiet") return /* @__PURE__ */ jsx("div", {
47524
+ className: "flex flex-col items-center justify-center gap-2 rounded-lg border border-border bg-surface p-4 min-h-[96px]",
47525
+ "data-testid": "sensor-hero-quiet",
47526
+ children: /* @__PURE__ */ jsx("span", {
47527
+ className: "text-sm text-foreground-subtle",
47528
+ children: device.name
47529
+ })
47530
+ });
47531
+ const slice = resolution.kind === "value" ? resolution.slice : void 0;
47428
47532
  if (descriptor === void 0 || slice === void 0) return /* @__PURE__ */ jsxs("div", {
47429
47533
  className: "flex flex-col items-center justify-center gap-2 rounded-lg border border-border bg-surface p-4 min-h-[96px]",
47430
47534
  children: [/* @__PURE__ */ jsx("span", {
@@ -47443,7 +47547,7 @@ function SensorHeroInner({ trpc, device }) {
47443
47547
  const rendered = resolveSensorDisplay({
47444
47548
  display: device.display,
47445
47549
  capName: device.role === "numeric-sensor" ? "numeric-sensor" : void 0,
47446
- roleDefault: device.role != null ? roleDefaultsData?.defaults[device.role] : roleDefaultsData?.defaults["temperature-sensor"],
47550
+ roleDefault: device.role != null ? roleDefaultsData?.defaults?.[device.role] : roleDefaultsData?.defaults?.["temperature-sensor"],
47447
47551
  quantity: descriptor.quantity ?? "device",
47448
47552
  deviceQuantity: deviceQuantity === "device" ? void 0 : deviceQuantity
47449
47553
  }).format(rawValue);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/ui-library",
3
- "version": "1.2.327",
3
+ "version": "1.2.328",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/camstack/server.git",