@camstack/ui-library 0.1.51 → 0.1.53

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/dist/index.cjs CHANGED
@@ -37,6 +37,10 @@ let _trpc_client = require("@trpc/client");
37
37
  _trpc_client = __toESM(_trpc_client, 1);
38
38
  let _trpc_react_query = require("@trpc/react-query");
39
39
  _trpc_react_query = __toESM(_trpc_react_query, 1);
40
+ let react_konva = require("react-konva");
41
+ react_konva = __toESM(react_konva, 1);
42
+ let konva = require("konva");
43
+ konva = __toESM(konva, 1);
40
44
  let _camstack_types = require("@camstack/types");
41
45
  let _camstack_sdk = require("@camstack/sdk");
42
46
  let zod = require("zod");
@@ -6875,6 +6879,8 @@ function populateShareCache() {
6875
6879
  cache.share["@tanstack/react-query"] ??= _tanstack_react_query;
6876
6880
  cache.share["@trpc/client"] ??= _trpc_client;
6877
6881
  cache.share["@trpc/react-query"] ??= _trpc_react_query;
6882
+ cache.share["react-konva"] ??= react_konva;
6883
+ cache.share["konva"] ??= konva;
6878
6884
  cache.share["@camstack/ui-library"] ??= readHostGlobal("__camstackUiLibrary");
6879
6885
  cache.share["@camstack/sdk"] ??= readHostGlobal("__camstackSdk");
6880
6886
  cache.share["@camstack/types"] ??= readHostGlobal("__camstackTypes");
@@ -6893,6 +6899,8 @@ function ensureMfHostInit() {
6893
6899
  "@tanstack/react-query": npmShared(() => _tanstack_react_query),
6894
6900
  "@trpc/client": npmShared(() => _trpc_client),
6895
6901
  "@trpc/react-query": npmShared(() => _trpc_react_query),
6902
+ "react-konva": npmShared(() => react_konva),
6903
+ "konva": npmShared(() => konva),
6896
6904
  "@camstack/ui-library": npmShared(() => readHostGlobal("__camstackUiLibrary")),
6897
6905
  "@camstack/sdk": npmShared(() => readHostGlobal("__camstackSdk")),
6898
6906
  "@camstack/types": npmShared(() => readHostGlobal("__camstackTypes"))
@@ -15806,6 +15814,8 @@ var useAddonsGetLastRestart = trpc.addons.getLastRestart.useQuery;
15806
15814
  var useAddonsListFrameworkPackages = trpc.addons.listFrameworkPackages.useQuery;
15807
15815
  /** Generated alias around `trpc.addons.listCapabilityProviders.useQuery`. */
15808
15816
  var useAddonsListCapabilityProviders = trpc.addons.listCapabilityProviders.useQuery;
15817
+ /** Generated alias around `trpc.addons.setCapabilityProviderEnabled.useMutation`. */
15818
+ var useAddonsSetCapabilityProviderEnabled = trpc.addons.setCapabilityProviderEnabled.useMutation;
15809
15819
  /** Generated alias around `trpc.addons.updateFrameworkPackage.useMutation`. */
15810
15820
  var useAddonsUpdateFrameworkPackage = trpc.addons.updateFrameworkPackage.useMutation;
15811
15821
  /** Generated alias around `trpc.addons.getVersions.useQuery`. */
@@ -15896,10 +15906,6 @@ var useAudioCodecListActiveSessions = trpc.audioCodec.listActiveSessions.useQuer
15896
15906
  var useAudioMetricsGetCurrentSnapshot = trpc.audioMetrics.getCurrentSnapshot.useQuery;
15897
15907
  /** Generated alias around `trpc.audioMetrics.getHistory.useQuery`. */
15898
15908
  var useAudioMetricsGetHistory = trpc.audioMetrics.getHistory.useQuery;
15899
- /** Generated alias around `trpc.authentication.listProviders.useQuery`. */
15900
- var useAuthenticationListProviders = trpc.authentication.listProviders.useQuery;
15901
- /** Generated alias around `trpc.authentication.setProviderEnabled.useMutation`. */
15902
- var useAuthenticationSetProviderEnabled = trpc.authentication.setProviderEnabled.useMutation;
15903
15909
  /** Generated alias around `trpc.backup.listDestinations.useQuery`. */
15904
15910
  var useBackupListDestinations = trpc.backup.listDestinations.useQuery;
15905
15911
  /** Generated alias around `trpc.backup.trigger.useMutation`. */
@@ -15950,6 +15956,12 @@ var useDecoderPushPacket = trpc.decoder.pushPacket.useQuery;
15950
15956
  var useDecoderOpenStream = trpc.decoder.openStream.useQuery;
15951
15957
  /** Generated alias around `trpc.decoder.pullFrames.useQuery`. */
15952
15958
  var useDecoderPullFrames = trpc.decoder.pullFrames.useQuery;
15959
+ /** Generated alias around `trpc.decoder.pullHandles.useQuery`. */
15960
+ var useDecoderPullHandles = trpc.decoder.pullHandles.useQuery;
15961
+ /** Generated alias around `trpc.decoder.getFrame.useQuery`. */
15962
+ var useDecoderGetFrame = trpc.decoder.getFrame.useQuery;
15963
+ /** Generated alias around `trpc.decoder.getShmStats.useQuery`. */
15964
+ var useDecoderGetShmStats = trpc.decoder.getShmStats.useQuery;
15953
15965
  /** Generated alias around `trpc.decoder.updateConfig.useQuery`. */
15954
15966
  var useDecoderUpdateConfig = trpc.decoder.updateConfig.useQuery;
15955
15967
  /** Generated alias around `trpc.decoder.getStats.useQuery`. */
@@ -16192,18 +16204,6 @@ var useMeshNetworkLogout = trpc.meshNetwork.logout.useMutation;
16192
16204
  var useMeshNetworkListPeers = trpc.meshNetwork.listPeers.useQuery;
16193
16205
  /** Generated alias around `trpc.meshNetwork.testConnection.useMutation`. */
16194
16206
  var useMeshNetworkTestConnection = trpc.meshNetwork.testConnection.useMutation;
16195
- /** Generated alias around `trpc.meshOrchestrator.listProviders.useQuery`. */
16196
- var useMeshOrchestratorListProviders = trpc.meshOrchestrator.listProviders.useQuery;
16197
- /** Generated alias around `trpc.meshOrchestrator.joinProvider.useMutation`. */
16198
- var useMeshOrchestratorJoinProvider = trpc.meshOrchestrator.joinProvider.useMutation;
16199
- /** Generated alias around `trpc.meshOrchestrator.leaveProvider.useMutation`. */
16200
- var useMeshOrchestratorLeaveProvider = trpc.meshOrchestrator.leaveProvider.useMutation;
16201
- /** Generated alias around `trpc.meshOrchestrator.startLoginProvider.useMutation`. */
16202
- var useMeshOrchestratorStartLoginProvider = trpc.meshOrchestrator.startLoginProvider.useMutation;
16203
- /** Generated alias around `trpc.meshOrchestrator.logoutProvider.useMutation`. */
16204
- var useMeshOrchestratorLogoutProvider = trpc.meshOrchestrator.logoutProvider.useMutation;
16205
- /** Generated alias around `trpc.meshOrchestrator.listProviderPeers.useQuery`. */
16206
- var useMeshOrchestratorListProviderPeers = trpc.meshOrchestrator.listProviderPeers.useQuery;
16207
16207
  /** Generated alias around `trpc.metricsProvider.collectSnapshot.useQuery`. */
16208
16208
  var useMetricsProviderCollectSnapshot = trpc.metricsProvider.collectSnapshot.useQuery;
16209
16209
  /** Generated alias around `trpc.metricsProvider.getCached.useQuery`. */
@@ -16246,6 +16246,12 @@ var useMotionDetectionApplyDeviceSettingsPatch = trpc.motionDetection.applyDevic
16246
16246
  var useMotionTriggerSetMotionTrigger = trpc.motionTrigger.setMotionTrigger.useMutation;
16247
16247
  /** Generated alias around `trpc.motionTrigger.getStatus.useQuery`. */
16248
16248
  var useMotionTriggerGetStatus = trpc.motionTrigger.getStatus.useQuery;
16249
+ /** Generated alias around `trpc.motionZones.getOptions.useQuery`. */
16250
+ var useMotionZonesGetOptions = trpc.motionZones.getOptions.useQuery;
16251
+ /** Generated alias around `trpc.motionZones.setZone.useMutation`. */
16252
+ var useMotionZonesSetZone = trpc.motionZones.setZone.useMutation;
16253
+ /** Generated alias around `trpc.motionZones.getStatus.useQuery`. */
16254
+ var useMotionZonesGetStatus = trpc.motionZones.getStatus.useQuery;
16249
16255
  /** Generated alias around `trpc.mqttBroker.listBrokers.useQuery`. */
16250
16256
  var useMqttBrokerListBrokers = trpc.mqttBroker.listBrokers.useQuery;
16251
16257
  /** Generated alias around `trpc.mqttBroker.getBrokerConfig.useQuery`. */
@@ -16264,6 +16270,16 @@ var useMqttBrokerStopEmbeddedBroker = trpc.mqttBroker.stopEmbeddedBroker.useMuta
16264
16270
  var useMqttBrokerGetStatus = trpc.mqttBroker.getStatus.useQuery;
16265
16271
  /** Generated alias around `trpc.nativeObjectDetection.getStatus.useQuery`. */
16266
16272
  var useNativeObjectDetectionGetStatus = trpc.nativeObjectDetection.getStatus.useQuery;
16273
+ /** Generated alias around `trpc.networkAccess.start.useMutation`. */
16274
+ var useNetworkAccessStart = trpc.networkAccess.start.useMutation;
16275
+ /** Generated alias around `trpc.networkAccess.stop.useMutation`. */
16276
+ var useNetworkAccessStop = trpc.networkAccess.stop.useMutation;
16277
+ /** Generated alias around `trpc.networkAccess.getEndpoint.useQuery`. */
16278
+ var useNetworkAccessGetEndpoint = trpc.networkAccess.getEndpoint.useQuery;
16279
+ /** Generated alias around `trpc.networkAccess.getStatus.useQuery`. */
16280
+ var useNetworkAccessGetStatus = trpc.networkAccess.getStatus.useQuery;
16281
+ /** Generated alias around `trpc.networkAccess.listEndpoints.useQuery`. */
16282
+ var useNetworkAccessListEndpoints = trpc.networkAccess.listEndpoints.useQuery;
16267
16283
  /** Generated alias around `trpc.networkQuality.getDeviceStats.useQuery`. */
16268
16284
  var useNetworkQualityGetDeviceStats = trpc.networkQuality.getDeviceStats.useQuery;
16269
16285
  /** Generated alias around `trpc.networkQuality.getAllStats.useQuery`. */
@@ -16288,6 +16304,8 @@ var useNodesShutdownNode = trpc.nodes.shutdownNode.useMutation;
16288
16304
  var useNodesRenameNode = trpc.nodes.renameNode.useMutation;
16289
16305
  /** Generated alias around `trpc.nodes.clusterAddonStatus.useQuery`. */
16290
16306
  var useNodesClusterAddonStatus = trpc.nodes.clusterAddonStatus.useQuery;
16307
+ /** Generated alias around `trpc.nodes.getCapUsageGraph.useQuery`. */
16308
+ var useNodesGetCapUsageGraph = trpc.nodes.getCapUsageGraph.useQuery;
16291
16309
  /** Generated alias around `trpc.nodes.getNodeAddons.useQuery`. */
16292
16310
  var useNodesGetNodeAddons = trpc.nodes.getNodeAddons.useQuery;
16293
16311
  /** Generated alias around `trpc.nodes.setProcessLogLevel.useMutation`. */
@@ -16506,10 +16524,18 @@ var usePtzStop = trpc.ptz.stop.useMutation;
16506
16524
  var usePtzGetPresets = trpc.ptz.getPresets.useQuery;
16507
16525
  /** Generated alias around `trpc.ptz.goToPreset.useMutation`. */
16508
16526
  var usePtzGoToPreset = trpc.ptz.goToPreset.useMutation;
16527
+ /** Generated alias around `trpc.ptz.savePreset.useMutation`. */
16528
+ var usePtzSavePreset = trpc.ptz.savePreset.useMutation;
16529
+ /** Generated alias around `trpc.ptz.deletePreset.useMutation`. */
16530
+ var usePtzDeletePreset = trpc.ptz.deletePreset.useMutation;
16531
+ /** Generated alias around `trpc.ptz.getOptions.useQuery`. */
16532
+ var usePtzGetOptions = trpc.ptz.getOptions.useQuery;
16509
16533
  /** Generated alias around `trpc.ptz.goHome.useMutation`. */
16510
16534
  var usePtzGoHome = trpc.ptz.goHome.useMutation;
16511
16535
  /** Generated alias around `trpc.ptz.getPosition.useQuery`. */
16512
16536
  var usePtzGetPosition = trpc.ptz.getPosition.useQuery;
16537
+ /** Generated alias around `trpc.ptz.setAutofocus.useMutation`. */
16538
+ var usePtzSetAutofocus = trpc.ptz.setAutofocus.useMutation;
16513
16539
  /** Generated alias around `trpc.ptz.getStatus.useQuery`. */
16514
16540
  var usePtzGetStatus = trpc.ptz.getStatus.useQuery;
16515
16541
  /** Generated alias around `trpc.ptzAutotrack.getStatus.useQuery`. */
@@ -16564,12 +16590,6 @@ var useRecordingEngineGetRetentionConfig = trpc.recordingEngine.getRetentionConf
16564
16590
  var useRecordingEngineUpdateRetentionConfig = trpc.recordingEngine.updateRetentionConfig.useMutation;
16565
16591
  /** Generated alias around `trpc.recordingEngine.getMotionStats.useQuery`. */
16566
16592
  var useRecordingEngineGetMotionStats = trpc.recordingEngine.getMotionStats.useQuery;
16567
- /** Generated alias around `trpc.remoteAccess.listProviders.useQuery`. */
16568
- var useRemoteAccessListProviders = trpc.remoteAccess.listProviders.useQuery;
16569
- /** Generated alias around `trpc.remoteAccess.startProvider.useMutation`. */
16570
- var useRemoteAccessStartProvider = trpc.remoteAccess.startProvider.useMutation;
16571
- /** Generated alias around `trpc.remoteAccess.stopProvider.useMutation`. */
16572
- var useRemoteAccessStopProvider = trpc.remoteAccess.stopProvider.useMutation;
16573
16593
  /** Generated alias around `trpc.settingsStore.get.useQuery`. */
16574
16594
  var useSettingsStoreGet = trpc.settingsStore.get.useQuery;
16575
16595
  /** Generated alias around `trpc.settingsStore.set.useMutation`. */
@@ -16672,8 +16692,18 @@ var useStreamBrokerGetStreamUrl = trpc.streamBroker.getStreamUrl.useQuery;
16672
16692
  var useStreamBrokerGetStreamWithCodec = trpc.streamBroker.getStreamWithCodec.useMutation;
16673
16693
  /** Generated alias around `trpc.streamBroker.releaseStreamWithCodec.useMutation`. */
16674
16694
  var useStreamBrokerReleaseStreamWithCodec = trpc.streamBroker.releaseStreamWithCodec.useMutation;
16675
- /** Generated alias around `trpc.streamBroker.getBroker.useQuery`. */
16676
- var useStreamBrokerGetBroker = trpc.streamBroker.getBroker.useQuery;
16695
+ /** Generated alias around `trpc.streamBroker.subscribeAudioChunks.useMutation`. */
16696
+ var useStreamBrokerSubscribeAudioChunks = trpc.streamBroker.subscribeAudioChunks.useMutation;
16697
+ /** Generated alias around `trpc.streamBroker.pullAudioChunks.useQuery`. */
16698
+ var useStreamBrokerPullAudioChunks = trpc.streamBroker.pullAudioChunks.useQuery;
16699
+ /** Generated alias around `trpc.streamBroker.unsubscribeAudioChunks.useMutation`. */
16700
+ var useStreamBrokerUnsubscribeAudioChunks = trpc.streamBroker.unsubscribeAudioChunks.useMutation;
16701
+ /** Generated alias around `trpc.streamBroker.subscribeFrames.useMutation`. */
16702
+ var useStreamBrokerSubscribeFrames = trpc.streamBroker.subscribeFrames.useMutation;
16703
+ /** Generated alias around `trpc.streamBroker.pullFrameHandles.useQuery`. */
16704
+ var useStreamBrokerPullFrameHandles = trpc.streamBroker.pullFrameHandles.useQuery;
16705
+ /** Generated alias around `trpc.streamBroker.unsubscribeFrames.useMutation`. */
16706
+ var useStreamBrokerUnsubscribeFrames = trpc.streamBroker.unsubscribeFrames.useMutation;
16677
16707
  /** Generated alias around `trpc.streamBroker.setPreBufferDuration.useMutation`. */
16678
16708
  var useStreamBrokerSetPreBufferDuration = trpc.streamBroker.setPreBufferDuration.useMutation;
16679
16709
  /** Generated alias around `trpc.streamBroker.getPreBufferInfo.useQuery`. */
@@ -16696,6 +16726,14 @@ var useStreamBrokerGetDeviceSettingsContribution = trpc.streamBroker.getDeviceSe
16696
16726
  var useStreamBrokerGetDeviceLiveContribution = trpc.streamBroker.getDeviceLiveContribution.useQuery;
16697
16727
  /** Generated alias around `trpc.streamBroker.applyDeviceSettingsPatch.useMutation`. */
16698
16728
  var useStreamBrokerApplyDeviceSettingsPatch = trpc.streamBroker.applyDeviceSettingsPatch.useMutation;
16729
+ /** Generated alias around `trpc.streamParams.getOptions.useQuery`. */
16730
+ var useStreamParamsGetOptions = trpc.streamParams.getOptions.useQuery;
16731
+ /** Generated alias around `trpc.streamParams.setProfile.useMutation`. */
16732
+ var useStreamParamsSetProfile = trpc.streamParams.setProfile.useMutation;
16733
+ /** Generated alias around `trpc.streamParams.getConfigSchema.useQuery`. */
16734
+ var useStreamParamsGetConfigSchema = trpc.streamParams.getConfigSchema.useQuery;
16735
+ /** Generated alias around `trpc.streamParams.getStatus.useQuery`. */
16736
+ var useStreamParamsGetStatus = trpc.streamParams.getStatus.useQuery;
16699
16737
  /** Generated alias around `trpc.switch.setState.useMutation`. */
16700
16738
  var useSwitchSetState = trpc.switch.setState.useMutation;
16701
16739
  /** Generated alias around `trpc.switch.getStatus.useQuery`. */
@@ -16716,12 +16754,6 @@ var useSystemSetRetentionConfig = trpc.system.setRetentionConfig.useMutation;
16716
16754
  var useSystemForceRetentionCleanup = trpc.system.forceRetentionCleanup.useMutation;
16717
16755
  /** Generated alias around `trpc.toast.onToast.useSubscription`. */
16718
16756
  var useToastOnToast = trpc.toast.onToast.useSubscription;
16719
- /** Generated alias around `trpc.turnOrchestrator.listProviders.useQuery`. */
16720
- var useTurnOrchestratorListProviders = trpc.turnOrchestrator.listProviders.useQuery;
16721
- /** Generated alias around `trpc.turnOrchestrator.getAllServers.useQuery`. */
16722
- var useTurnOrchestratorGetAllServers = trpc.turnOrchestrator.getAllServers.useQuery;
16723
- /** Generated alias around `trpc.turnOrchestrator.setProviderEnabled.useMutation`. */
16724
- var useTurnOrchestratorSetProviderEnabled = trpc.turnOrchestrator.setProviderEnabled.useMutation;
16725
16757
  /** Generated alias around `trpc.turnProvider.getTurnServers.useQuery`. */
16726
16758
  var useTurnProviderGetTurnServers = trpc.turnProvider.getTurnServers.useQuery;
16727
16759
  /** Generated alias around `trpc.userManagement.listUsers.useQuery`. */
@@ -16764,6 +16796,18 @@ var useUserManagementDisableTotp = trpc.userManagement.disableTotp.useMutation;
16764
16796
  var useUserManagementGetTotpStatus = trpc.userManagement.getTotpStatus.useQuery;
16765
16797
  /** Generated alias around `trpc.userManagement.verifyTotp.useMutation`. */
16766
16798
  var useUserManagementVerifyTotp = trpc.userManagement.verifyTotp.useMutation;
16799
+ /** Generated alias around `trpc.userManagement.oauthIssueCode.useMutation`. */
16800
+ var useUserManagementOauthIssueCode = trpc.userManagement.oauthIssueCode.useMutation;
16801
+ /** Generated alias around `trpc.userManagement.oauthExchangeCode.useMutation`. */
16802
+ var useUserManagementOauthExchangeCode = trpc.userManagement.oauthExchangeCode.useMutation;
16803
+ /** Generated alias around `trpc.userManagement.oauthRefresh.useMutation`. */
16804
+ var useUserManagementOauthRefresh = trpc.userManagement.oauthRefresh.useMutation;
16805
+ /** Generated alias around `trpc.userManagement.oauthVerifyAccessToken.useQuery`. */
16806
+ var useUserManagementOauthVerifyAccessToken = trpc.userManagement.oauthVerifyAccessToken.useQuery;
16807
+ /** Generated alias around `trpc.userManagement.listOauthSessions.useQuery`. */
16808
+ var useUserManagementListOauthSessions = trpc.userManagement.listOauthSessions.useQuery;
16809
+ /** Generated alias around `trpc.userManagement.revokeOauthSession.useMutation`. */
16810
+ var useUserManagementRevokeOauthSession = trpc.userManagement.revokeOauthSession.useMutation;
16767
16811
  /** Generated alias around `trpc.webrtcSession.listStreams.useQuery`. */
16768
16812
  var useWebrtcSessionListStreams = trpc.webrtcSession.listStreams.useQuery;
16769
16813
  /** Generated alias around `trpc.webrtcSession.createSession.useMutation`. */
@@ -16807,8 +16851,16 @@ var useZonesUpdateZone = trpc.zones.updateZone.useMutation;
16807
16851
  * `vite.widgets.config.ts`). At runtime we fetch the public list of
16808
16852
  * addon-contributed widgets via `useAddonWidgetsListWidgets()`, register
16809
16853
  * each remote (`registerRemotes`) once, then resolve a widget by
16810
- * loading the exposed `./widgets` module (`loadRemote`) whose default
16811
- * export is a `Record<stableId, ComponentType<WidgetProps>>` map.
16854
+ * loading the exposed module (`loadRemote`) whose default export is a
16855
+ * `Record<componentKey, ComponentType>` map.
16856
+ *
16857
+ * Unified UI-contribution model (Task 10) — a widget declaration IS a
16858
+ * `UiContribution` with `kind:'remote'`. Both the device-detail path
16859
+ * (`ContributionRenderer`) and the dashboard render widgets through the
16860
+ * same `loadRemoteBundle` MF path. `loadRemoteBundle` is exported and
16861
+ * generic over `(remoteName, exposedModule, entryUrl)` so any
16862
+ * `UiContributionRemote` descriptor resolves without an aggregator-keyed
16863
+ * lookup.
16812
16864
  *
16813
16865
  * Why MF instead of raw ESM:
16814
16866
  * - automatic dedup of shared deps (react/react-dom/@tanstack/etc.)
@@ -16818,9 +16870,7 @@ var useZonesUpdateZone = trpc.zones.updateZone.useMutation;
16818
16870
  * wins by default).
16819
16871
  * - cross-bundle context invariant preserved automatically: every
16820
16872
  * remote's `@camstack/ui-library` import resolves to the host's
16821
- * instance, so `createContext()` references match across bundles
16822
- * (the duplicate-Context bug that motivated `createSharedContext`
16823
- * falls out for free).
16873
+ * instance, so `createContext()` references match across bundles.
16824
16874
  *
16825
16875
  * Live-update — listens to `addon.widget-ready` so newly-loaded addons
16826
16876
  * surface their widgets without a manual page reload (the aggregator
@@ -16831,11 +16881,11 @@ var useZonesUpdateZone = trpc.zones.updateZone.useMutation;
16831
16881
  */
16832
16882
  var WidgetRegistryContext = createSharedContext("camstack:widget-registry", null);
16833
16883
  /**
16834
- * Process-global cache for resolved widget bundles, keyed by
16835
- * `remoteName` (every remote exposes a single `./widgets` module whose
16836
- * default export is the `Record<stableId, Component>` map). Survives
16837
- * provider remount — avoids a full re-fetch + re-init of the MF remote
16838
- * when admin-ui's tree cycles.
16884
+ * Process-global cache for resolved remote bundles, keyed by
16885
+ * `<remoteName>::<exposedModule>`. Each cached value is the exposed
16886
+ * module's default-record map (`Record<componentKey, Component>`).
16887
+ * Survives provider remount — avoids a full re-fetch + re-init of the
16888
+ * MF remote when admin-ui's tree cycles.
16839
16889
  */
16840
16890
  var bundleModuleCache = /* @__PURE__ */ new Map();
16841
16891
  var bundleInflight = /* @__PURE__ */ new Map();
@@ -16847,6 +16897,10 @@ var bundleInflight = /* @__PURE__ */ new Map();
16847
16897
  * update.
16848
16898
  */
16849
16899
  var registeredRemotes = /* @__PURE__ */ new Set();
16900
+ /** Cache key for `bundleModuleCache` — one entry per `(remote, module)`. */
16901
+ function bundleCacheKey(remoteName, exposedModule) {
16902
+ return `${remoteName}::${exposedModule}`;
16903
+ }
16850
16904
  function isRemoteWidgetsModule(value) {
16851
16905
  if (!value || typeof value !== "object") return false;
16852
16906
  const def = value.default;
@@ -16855,8 +16909,7 @@ function isRemoteWidgetsModule(value) {
16855
16909
  /**
16856
16910
  * Diagnostic-only string for the "Got: …" tail in the not-a-record
16857
16911
  * error. Returns a JSON-friendly snapshot of the module's keys + proto
16858
- * name without exposing the value itself (the value is the unknown
16859
- * thing the module factory produced, may be huge or contain PII).
16912
+ * name without exposing the value itself.
16860
16913
  */
16861
16914
  function describeRemoteShape(mod) {
16862
16915
  if (!mod || typeof mod !== "object") return typeof mod;
@@ -16870,13 +16923,19 @@ function describeRemoteShape(mod) {
16870
16923
  };
16871
16924
  }
16872
16925
  /**
16873
- * Register the MF remote (idempotent) and load its `./widgets` module.
16874
- * Returns the resolved `Record<stableId, Component>` map.
16926
+ * Register the MF remote (idempotent) and load one of its exposed
16927
+ * modules. Returns the resolved `Record<componentKey, Component>` map
16928
+ * (the exposed module's `default` export).
16929
+ *
16930
+ * Exported so the unified `ContributionRenderer` `kind:'remote'` path
16931
+ * can load an arbitrary `{ remoteName, exposedModule }` descriptor —
16932
+ * the same path the `WidgetRegistry` uses internally for widgets.
16875
16933
  */
16876
- async function loadRemoteBundle(remoteName, entryUrl) {
16877
- const cached = bundleModuleCache.get(remoteName);
16934
+ async function loadRemoteBundle(remoteName, exposedModule, entryUrl) {
16935
+ const cacheKey = bundleCacheKey(remoteName, exposedModule);
16936
+ const cached = bundleModuleCache.get(cacheKey);
16878
16937
  if (cached) return cached;
16879
- const inflight = bundleInflight.get(remoteName);
16938
+ const inflight = bundleInflight.get(cacheKey);
16880
16939
  if (inflight) return inflight;
16881
16940
  if (!registeredRemotes.has(remoteName)) {
16882
16941
  ensureMfHostInit();
@@ -16887,22 +16946,26 @@ async function loadRemoteBundle(remoteName, entryUrl) {
16887
16946
  }], { force: false });
16888
16947
  registeredRemotes.add(remoteName);
16889
16948
  }
16890
- const promise = loadRemote(`${remoteName}/widgets`).then((mod) => {
16949
+ const promise = loadRemote(`${remoteName}/${exposedModule.startsWith("./") ? exposedModule.slice(2) : exposedModule}`).then((mod) => {
16891
16950
  if (!isRemoteWidgetsModule(mod)) {
16892
16951
  const shape = describeRemoteShape(mod);
16893
- throw new Error(`Widget remote ${remoteName} (${entryUrl}) does not expose a default record on './widgets'. Got: ${JSON.stringify(shape)}`);
16952
+ throw new Error(`Remote ${remoteName} (${entryUrl}) does not expose a default record on '${exposedModule}'. Got: ${JSON.stringify(shape)}`);
16894
16953
  }
16895
16954
  const map = mod.default;
16896
- bundleModuleCache.set(remoteName, map);
16897
- bundleInflight.delete(remoteName);
16955
+ bundleModuleCache.set(cacheKey, map);
16956
+ bundleInflight.delete(cacheKey);
16898
16957
  return map;
16899
16958
  }).catch((err) => {
16900
- bundleInflight.delete(remoteName);
16959
+ bundleInflight.delete(cacheKey);
16901
16960
  throw err;
16902
16961
  });
16903
- bundleInflight.set(remoteName, promise);
16962
+ bundleInflight.set(cacheKey, promise);
16904
16963
  return promise;
16905
16964
  }
16965
+ /** Synchronous peek at the resolved-bundle cache — `undefined` if not yet loaded. */
16966
+ function peekBundle(remoteName, exposedModule) {
16967
+ return bundleModuleCache.get(bundleCacheKey(remoteName, exposedModule));
16968
+ }
16906
16969
  var BOOT_WINDOW_MS = 3e4;
16907
16970
  var POLL_INTERVAL_MS = 2e3;
16908
16971
  function WidgetRegistryProvider({ children }) {
@@ -16922,21 +16985,24 @@ function WidgetRegistryProvider({ children }) {
16922
16985
  queryClient.invalidateQueries({ queryKey: [["addonWidgets", "listWidgets"]] });
16923
16986
  });
16924
16987
  const [resolvedTick, setResolvedTick] = (0, react$1.useState)(0);
16988
+ const widgets = (0, react$1.useMemo)(() => rawWidgets ?? [], [rawWidgets]);
16925
16989
  (0, react$1.useEffect)(() => {
16926
- if (!rawWidgets) return;
16990
+ if (widgets.length === 0) return;
16927
16991
  let cancelled = false;
16928
- const seenRemotes = /* @__PURE__ */ new Set();
16929
- for (const w of rawWidgets) {
16930
- if (seenRemotes.has(w.remoteName)) continue;
16931
- seenRemotes.add(w.remoteName);
16932
- if (bundleModuleCache.has(w.remoteName)) continue;
16992
+ const seen = /* @__PURE__ */ new Set();
16993
+ for (const w of widgets) {
16994
+ const key = bundleCacheKey(w.remote.remoteName, w.remote.exposedModule);
16995
+ if (seen.has(key)) continue;
16996
+ seen.add(key);
16997
+ if (bundleModuleCache.has(key)) continue;
16933
16998
  const entryUrl = w.bundleUrl;
16934
- loadRemoteBundle(w.remoteName, entryUrl).then(() => {
16999
+ loadRemoteBundle(w.remote.remoteName, w.remote.exposedModule, entryUrl).then(() => {
16935
17000
  if (!cancelled) setResolvedTick((t) => t + 1);
16936
17001
  }).catch((err) => {
16937
17002
  const reason = err instanceof Error ? err.message : String(err);
16938
17003
  (typeof globalThis !== "undefined" ? globalThis.console : void 0)?.error?.("[WidgetRegistry] Failed to load widget remote", {
16939
- remoteName: w.remoteName,
17004
+ remoteName: w.remote.remoteName,
17005
+ exposedModule: w.remote.exposedModule,
16940
17006
  entryUrl,
16941
17007
  reason
16942
17008
  });
@@ -16945,19 +17011,26 @@ function WidgetRegistryProvider({ children }) {
16945
17011
  return () => {
16946
17012
  cancelled = true;
16947
17013
  };
16948
- }, [rawWidgets]);
17014
+ }, [widgets]);
16949
17015
  const registry = (0, react$1.useMemo)(() => {
16950
- const widgets = rawWidgets ?? [];
16951
17016
  const byId = /* @__PURE__ */ new Map();
16952
- for (const w of widgets) byId.set(`${w.addonId}/${w.stableId}`, w);
17017
+ const entryUrlByRemote = /* @__PURE__ */ new Map();
17018
+ for (const w of widgets) {
17019
+ byId.set(`${w.addonId}/${w.stableId}`, w);
17020
+ entryUrlByRemote.set(w.remote.remoteName, w.bundleUrl);
17021
+ }
16953
17022
  const toMetadata = (widgetId, entry) => ({
16954
17023
  widgetId,
16955
17024
  addonId: entry.addonId,
16956
17025
  stableId: entry.stableId,
17026
+ tab: entry.tab,
17027
+ subTab: entry.subTab,
16957
17028
  label: entry.label,
17029
+ order: entry.order,
17030
+ kind: entry.kind,
17031
+ remote: entry.remote,
16958
17032
  description: entry.description,
16959
17033
  icon: entry.icon,
16960
- remoteName: entry.remoteName,
16961
17034
  bundleUrl: entry.bundleUrl,
16962
17035
  hosts: entry.hosts,
16963
17036
  requires: entry.requires,
@@ -16970,9 +17043,9 @@ function WidgetRegistryProvider({ children }) {
16970
17043
  resolve: (widgetId) => {
16971
17044
  const entry = byId.get(widgetId);
16972
17045
  if (!entry) return void 0;
16973
- const bundle = bundleModuleCache.get(entry.remoteName);
17046
+ const bundle = peekBundle(entry.remote.remoteName, entry.remote.exposedModule);
16974
17047
  if (!bundle) return null;
16975
- const Component = bundle[entry.stableId];
17048
+ const Component = bundle[entry.remote.componentKey ?? entry.stableId];
16976
17049
  if (!Component) return void 0;
16977
17050
  return Component;
16978
17051
  },
@@ -16985,15 +17058,23 @@ function WidgetRegistryProvider({ children }) {
16985
17058
  const out = [];
16986
17059
  for (const [widgetId, entry] of byId) out.push(toMetadata(widgetId, entry));
16987
17060
  return out;
16988
- }
17061
+ },
17062
+ entryUrlFor: (remoteName) => entryUrlByRemote.get(remoteName)
16989
17063
  };
16990
- }, [rawWidgets, resolvedTick]);
17064
+ }, [widgets, resolvedTick]);
16991
17065
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetRegistryContext.Provider, {
16992
17066
  value: registry,
16993
17067
  children
16994
17068
  });
16995
17069
  }
16996
- /** Returns the registered widget component, `null` while loading, or `undefined` if unknown. */
17070
+ /**
17071
+ * Returns the registered widget component, `null` while loading, or
17072
+ * `undefined` if unknown.
17073
+ *
17074
+ * @deprecated Unused by all render paths since the unified
17075
+ * UI-contribution model (Task 10). Use `useRemoteComponent` /
17076
+ * `<WidgetSlot>` instead. Kept for external addons that may import it.
17077
+ */
16997
17078
  function useWidget(widgetId) {
16998
17079
  return useWidgetRegistry().resolve(widgetId);
16999
17080
  }
@@ -17007,6 +17088,68 @@ function useWidgetMetadata(widgetId) {
17007
17088
  function useAllWidgets() {
17008
17089
  return useWidgetRegistry().listAll();
17009
17090
  }
17091
+ /**
17092
+ * Resolve a `kind:'remote'` `UiContributionRemote` descriptor to a
17093
+ * React component. Drives the unified `ContributionRenderer`
17094
+ * `kind:'remote'` path — both device-detail contributions and dashboard
17095
+ * widgets resolve through the same MF `loadRemoteBundle` loader.
17096
+ *
17097
+ * Returns:
17098
+ * - `undefined` — the remote couldn't be resolved (no entry URL known,
17099
+ * or the remote exposed no component for `componentKey`),
17100
+ * - `null` — the bundle is still loading,
17101
+ * - the component otherwise.
17102
+ *
17103
+ * `entryUrl` may be passed explicitly; when omitted it's resolved from
17104
+ * the registry's aggregator-stamped `bundleUrl` for `remote.remoteName`.
17105
+ */
17106
+ function useRemoteComponent(remote, entryUrl) {
17107
+ const reg = useOptionalWidgetRegistry();
17108
+ const resolvedEntryUrl = entryUrl ?? reg?.entryUrlFor(remote.remoteName);
17109
+ const [tick, setTick] = (0, react$1.useState)(0);
17110
+ const [loadFailed, setLoadFailed] = (0, react$1.useState)(false);
17111
+ (0, react$1.useEffect)(() => {
17112
+ if (!resolvedEntryUrl) return;
17113
+ setLoadFailed(false);
17114
+ if (peekBundle(remote.remoteName, remote.exposedModule)) return;
17115
+ let cancelled = false;
17116
+ loadRemoteBundle(remote.remoteName, remote.exposedModule, resolvedEntryUrl).then(() => {
17117
+ if (!cancelled) setTick((t) => t + 1);
17118
+ }).catch((err) => {
17119
+ const reason = err instanceof Error ? err.message : String(err);
17120
+ (typeof globalThis !== "undefined" ? globalThis.console : void 0)?.error?.("[WidgetRegistry] Failed to load remote component", {
17121
+ remoteName: remote.remoteName,
17122
+ exposedModule: remote.exposedModule,
17123
+ entryUrl: resolvedEntryUrl,
17124
+ reason
17125
+ });
17126
+ if (!cancelled) setLoadFailed(true);
17127
+ });
17128
+ return () => {
17129
+ cancelled = true;
17130
+ };
17131
+ }, [
17132
+ remote.remoteName,
17133
+ remote.exposedModule,
17134
+ resolvedEntryUrl
17135
+ ]);
17136
+ return (0, react$1.useMemo)(() => {
17137
+ if (!resolvedEntryUrl) return void 0;
17138
+ if (loadFailed) return void 0;
17139
+ const bundle = peekBundle(remote.remoteName, remote.exposedModule);
17140
+ if (!bundle) return null;
17141
+ const componentKey = remote.componentKey;
17142
+ if (componentKey === void 0) return Object.values(bundle)[0] ?? void 0;
17143
+ return bundle[componentKey] ?? void 0;
17144
+ }, [
17145
+ remote.remoteName,
17146
+ remote.exposedModule,
17147
+ remote.componentKey,
17148
+ resolvedEntryUrl,
17149
+ tick,
17150
+ loadFailed
17151
+ ]);
17152
+ }
17010
17153
  /** Read the registry instance — throws when no provider is mounted. */
17011
17154
  function useWidgetRegistry() {
17012
17155
  const ctx = useOptionalWidgetRegistry();
@@ -17021,163 +17164,1429 @@ function useContextSafe(ctx) {
17021
17164
  return (0, react$1.useContext)(ctx);
17022
17165
  }
17023
17166
  //#endregion
17024
- //#region src/composites/widget-slot.tsx
17167
+ //#region src/hooks/use-ptz.ts
17025
17168
  /**
17026
- * <WidgetSlot>single host-side mount point for any addon-contributed
17027
- * widget. Consumers reference a widget by its public id
17028
- * (`<addonId>/<stableId>`), the slot looks the component up in the
17029
- * shared `WidgetRegistry`, validates host context against the widget's
17030
- * `requires` metadata, and renders one of:
17031
- * - skeleton placeholder while the bundle is still loading,
17032
- * - inline error fallback when the widget id is unknown OR the host
17033
- * didn't supply a required context (`deviceContext` /
17034
- * `integrationContext`),
17035
- * - the resolved component otherwise.
17169
+ * usePTZPTZ control hook for device-scoped pan / tilt / zoom.
17036
17170
  *
17037
- * The slot is intentionally STUPID no styling beyond the skeleton/
17038
- * error fallback. Layout (card vs inline, sizing) is the host's
17039
- * responsibility.
17171
+ * Wraps the `ptz` capability methods through the canonical
17172
+ * `useDeviceProxy` surface every call goes through
17173
+ * `dev.ptz?.<method>(...)` with deviceId/nodeId auto-injected. The
17174
+ * hook stays bare-bones around state (`busy`, `error`, `presets`)
17175
+ * so the operator-facing component (PTZOverlay) stays trivial.
17176
+ *
17177
+ * Returns:
17178
+ * - `move(direction)`: discrete one-shot move (cap.move + cap.stop).
17179
+ * Use for short pulse moves triggered by tapping a d-pad button.
17180
+ * - `startContinuous(direction)` / `stopContinuous()`: gesture-driven
17181
+ * continuous motion (`cap.continuousMove` + `cap.stop`). Use for
17182
+ * long-press handlers on touch / mouse-down handlers on desktop.
17183
+ * - `zoom('in' | 'out')`: discrete zoom step.
17184
+ * - `goHome()`: jump to preset 0.
17185
+ * - `presets` + `goToPreset(presetId)`: list and jump to named presets.
17186
+ *
17187
+ * Parametrised by the same `UseDeviceProxyTrpc` shape every other
17188
+ * device hook uses — works under admin-ui (BackendClient.trpc) and
17189
+ * addon pages (AddonPageProps.trpc) alike.
17040
17190
  */
17041
- function WidgetSlot(props) {
17042
- const { widgetId, host = "device-tab", config, deviceId, integrationId, instanceId, size, columns, rows } = props;
17043
- const Component = useWidget(widgetId);
17044
- const metadata = useWidgetMetadata(widgetId);
17045
- const resolvedInstanceId = (0, react$1.useMemo)(() => instanceId ?? widgetId, [instanceId, widgetId]);
17046
- if (Component === void 0 && metadata === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetMissingError, {
17047
- widgetId,
17048
- reason: "unknown"
17049
- });
17050
- if (Component === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetMissingError, {
17051
- widgetId,
17052
- reason: "missing-export"
17053
- });
17054
- if (Component === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetSkeleton, {});
17055
- if (metadata) {
17056
- if (metadata.requires.deviceContext && deviceId === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetMissingError, {
17057
- widgetId,
17058
- reason: "missing-device-context"
17059
- });
17060
- if (metadata.requires.integrationContext && integrationId === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetMissingError, {
17061
- widgetId,
17062
- reason: "missing-integration-context"
17063
- });
17191
+ var DIRECTION_VECTORS = {
17192
+ "up": {
17193
+ pan: 0,
17194
+ tilt: 1
17195
+ },
17196
+ "down": {
17197
+ pan: 0,
17198
+ tilt: -1
17199
+ },
17200
+ "left": {
17201
+ pan: -1,
17202
+ tilt: 0
17203
+ },
17204
+ "right": {
17205
+ pan: 1,
17206
+ tilt: 0
17207
+ },
17208
+ "up-left": {
17209
+ pan: -1,
17210
+ tilt: 1
17211
+ },
17212
+ "up-right": {
17213
+ pan: 1,
17214
+ tilt: 1
17215
+ },
17216
+ "down-left": {
17217
+ pan: -1,
17218
+ tilt: -1
17219
+ },
17220
+ "down-right": {
17221
+ pan: 1,
17222
+ tilt: -1
17064
17223
  }
17065
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Component, {
17066
- instanceId: resolvedInstanceId,
17067
- host,
17068
- config,
17069
- deviceId,
17070
- integrationId,
17071
- size,
17072
- columns,
17073
- rows
17074
- });
17075
- }
17076
- function WidgetSkeleton() {
17077
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
17078
- className: "rounded-lg border border-border bg-surface/40 p-4 animate-pulse",
17079
- children: [
17080
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "h-3 w-24 bg-foreground-subtle/20 rounded mb-2" }),
17081
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "h-2 w-full bg-foreground-subtle/10 rounded mb-1" }),
17082
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "h-2 w-3/4 bg-foreground-subtle/10 rounded" })
17083
- ]
17084
- });
17085
- }
17086
- function WidgetMissingError({ widgetId, reason }) {
17087
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
17088
- className: "rounded-lg border border-warning/30 bg-warning/10 px-3 py-2 text-xs text-warning",
17089
- children: reason === "unknown" ? `Widget "${widgetId}" not registered` : reason === "missing-export" ? `Widget "${widgetId}" bundle does not export this stableId` : reason === "missing-device-context" ? `Widget "${widgetId}" requires a deviceId` : `Widget "${widgetId}" requires an integrationId`
17090
- });
17091
- }
17092
- //#endregion
17093
- //#region src/composites/config-form-field.tsx
17094
- var INPUT_CLASS = "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";
17095
- var LABEL_CLASS = "block text-[11px] font-medium text-foreground mb-1";
17096
- var DESC_CLASS = "text-[10px] text-foreground-subtle mt-0.5";
17097
- function FieldWrapper({ label, description, required, span, children, translationFn }) {
17098
- const colSpanClass = span === 2 ? "col-span-2" : span === 3 ? "col-span-3" : span === 4 ? "col-span-4" : "col-span-1";
17099
- const resolvedLabel = resolveLabel(label, translationFn);
17100
- const resolvedDescription = resolveLabel(description, translationFn);
17101
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
17102
- className: colSpanClass,
17103
- children: [
17104
- resolvedLabel !== void 0 && resolvedLabel !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
17105
- className: LABEL_CLASS,
17106
- children: [resolvedLabel, required && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
17107
- className: "text-danger ml-0.5",
17108
- children: "*"
17109
- })]
17110
- }),
17111
- children,
17112
- resolvedDescription && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
17113
- className: DESC_CLASS,
17114
- children: resolvedDescription
17115
- })
17116
- ]
17117
- });
17118
- }
17119
- function TextField({ field, value, onChange, disabled, translationFn }) {
17120
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldWrapper, {
17121
- label: field.label,
17122
- description: field.description,
17123
- required: field.required,
17124
- span: field.span,
17125
- translationFn,
17126
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
17127
- type: field.inputType ?? "text",
17128
- className: INPUT_CLASS,
17129
- value: value === void 0 || value === null ? "" : String(value),
17130
- placeholder: field.placeholder,
17131
- maxLength: field.maxLength,
17132
- pattern: field.pattern,
17133
- disabled: disabled || field.disabled,
17134
- onChange: (e) => onChange(e.target.value)
17135
- })
17136
- });
17137
- }
17138
- function NumberField({ field, value, onChange, disabled, translationFn }) {
17139
- const [local, setLocal] = (0, react$1.useState)(value === void 0 || value === null ? "" : String(value));
17140
- const focusedRef = (0, react$1.useRef)(false);
17224
+ };
17225
+ function usePTZ(trpc, deviceId, hookOptions) {
17226
+ const defaultSpeed = hookOptions?.defaultSpeed ?? .5;
17227
+ const pulseMs = hookOptions?.pulseMs ?? 250;
17228
+ const enabled = hookOptions?.enabled ?? true;
17229
+ const ptz = useDeviceProxy(trpc, enabled ? deviceId : null)?.ptz;
17230
+ const [presets, setPresets] = (0, react$1.useState)([]);
17231
+ const [options, setOptions] = (0, react$1.useState)(null);
17232
+ const [busy, setBusy] = (0, react$1.useState)(false);
17233
+ const [error, setError] = (0, react$1.useState)(null);
17234
+ const isAbsentProvider = (err) => {
17235
+ const msg = err instanceof Error ? err.message : String(err);
17236
+ return msg.includes("provider not available") || msg.includes("no 'ptz' binding");
17237
+ };
17238
+ const refreshPresets = (0, react$1.useCallback)(async () => {
17239
+ if (!enabled || !ptz) return;
17240
+ try {
17241
+ setPresets(await ptz.getPresets({}));
17242
+ } catch (err) {
17243
+ if (isAbsentProvider(err)) return;
17244
+ setError(err instanceof Error ? err.message : String(err));
17245
+ }
17246
+ }, [ptz, enabled]);
17247
+ const refreshOptions = (0, react$1.useCallback)(async () => {
17248
+ if (!enabled || !ptz) return;
17249
+ try {
17250
+ setOptions(await ptz.getOptions({}));
17251
+ } catch (err) {
17252
+ if (isAbsentProvider(err)) return;
17253
+ setError(err instanceof Error ? err.message : String(err));
17254
+ }
17255
+ }, [ptz, enabled]);
17141
17256
  (0, react$1.useEffect)(() => {
17142
- if (focusedRef.current) return;
17143
- setLocal(value === void 0 || value === null ? "" : String(value));
17144
- }, [value]);
17145
- const handleChange = (raw) => {
17146
- setLocal(raw);
17147
- if (raw === "" || raw === "-") {
17148
- onChange(void 0);
17257
+ refreshPresets();
17258
+ refreshOptions();
17259
+ }, [refreshPresets, refreshOptions]);
17260
+ const wrap = (0, react$1.useCallback)(async (fn) => {
17261
+ if (!enabled || !ptz) return void 0;
17262
+ setError(null);
17263
+ setBusy(true);
17264
+ try {
17265
+ return await fn();
17266
+ } catch (err) {
17267
+ setError(err instanceof Error ? err.message : String(err));
17149
17268
  return;
17269
+ } finally {
17270
+ setBusy(false);
17150
17271
  }
17151
- const parsed = Number(raw);
17152
- if (Number.isNaN(parsed)) return;
17153
- onChange(parsed);
17154
- };
17155
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldWrapper, {
17156
- label: field.label,
17157
- description: field.description,
17158
- required: field.required,
17159
- span: field.span,
17160
- translationFn,
17161
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
17162
- className: "flex items-center gap-1",
17163
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
17164
- type: "number",
17165
- className: INPUT_CLASS,
17166
- value: local,
17167
- placeholder: field.placeholder,
17168
- min: field.min,
17169
- max: field.max,
17170
- step: field.step,
17171
- disabled: disabled || field.disabled,
17172
- onFocus: () => {
17173
- focusedRef.current = true;
17174
- },
17175
- onBlur: () => {
17176
- focusedRef.current = false;
17177
- },
17178
- onChange: (e) => handleChange(e.target.value)
17179
- }), field.unit && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
17180
- className: "text-xs text-foreground-subtle whitespace-nowrap",
17272
+ }, [enabled, ptz]);
17273
+ return {
17274
+ move: (0, react$1.useCallback)(async (direction, speed) => {
17275
+ if (!ptz) return;
17276
+ const v = DIRECTION_VECTORS[direction];
17277
+ const s = speed ?? defaultSpeed;
17278
+ await wrap(async () => {
17279
+ await ptz.move({
17280
+ pan: v.pan,
17281
+ tilt: v.tilt,
17282
+ speed: s
17283
+ });
17284
+ await new Promise((r) => setTimeout(r, pulseMs));
17285
+ await ptz.stop({});
17286
+ });
17287
+ }, [
17288
+ ptz,
17289
+ defaultSpeed,
17290
+ pulseMs,
17291
+ wrap
17292
+ ]),
17293
+ startContinuous: (0, react$1.useCallback)(async (direction, speed) => {
17294
+ if (!ptz) return;
17295
+ const v = DIRECTION_VECTORS[direction];
17296
+ const s = speed ?? defaultSpeed;
17297
+ await wrap(() => ptz.continuousMove({
17298
+ pan: v.pan,
17299
+ tilt: v.tilt,
17300
+ speed: s
17301
+ }));
17302
+ }, [
17303
+ ptz,
17304
+ defaultSpeed,
17305
+ wrap
17306
+ ]),
17307
+ stopContinuous: (0, react$1.useCallback)(async () => {
17308
+ if (!ptz) return;
17309
+ await wrap(() => ptz.stop({}));
17310
+ }, [ptz, wrap]),
17311
+ zoom: (0, react$1.useCallback)(async (direction, speed) => {
17312
+ if (!ptz) return;
17313
+ const z = direction === "in" ? 1 : -1;
17314
+ const s = speed ?? defaultSpeed;
17315
+ await wrap(async () => {
17316
+ await ptz.move({
17317
+ zoom: z,
17318
+ speed: s
17319
+ });
17320
+ await new Promise((r) => setTimeout(r, pulseMs));
17321
+ await ptz.stop({});
17322
+ });
17323
+ }, [
17324
+ ptz,
17325
+ defaultSpeed,
17326
+ pulseMs,
17327
+ wrap
17328
+ ]),
17329
+ goHome: (0, react$1.useCallback)(async () => {
17330
+ if (!ptz) return;
17331
+ await wrap(() => ptz.goHome({}));
17332
+ }, [ptz, wrap]),
17333
+ goToPreset: (0, react$1.useCallback)(async (presetId) => {
17334
+ if (!ptz) return;
17335
+ await wrap(() => ptz.goToPreset({ presetId }));
17336
+ }, [ptz, wrap]),
17337
+ savePreset: (0, react$1.useCallback)(async (name) => {
17338
+ if (!ptz) return void 0;
17339
+ return wrap(async () => {
17340
+ const usedIds = new Set(presets.map((p) => Number(p.id)).filter((n) => Number.isInteger(n) && n >= 0));
17341
+ let nextId = 0;
17342
+ while (usedIds.has(nextId)) nextId += 1;
17343
+ const presetId = String(nextId);
17344
+ await ptz.savePreset({
17345
+ presetId,
17346
+ name
17347
+ });
17348
+ await refreshPresets();
17349
+ return presetId;
17350
+ });
17351
+ }, [
17352
+ ptz,
17353
+ presets,
17354
+ refreshPresets,
17355
+ wrap
17356
+ ]),
17357
+ deletePreset: (0, react$1.useCallback)(async (presetId) => {
17358
+ if (!ptz) return;
17359
+ await wrap(async () => {
17360
+ await ptz.deletePreset({ presetId });
17361
+ await refreshPresets();
17362
+ });
17363
+ }, [
17364
+ ptz,
17365
+ refreshPresets,
17366
+ wrap
17367
+ ]),
17368
+ presets,
17369
+ refreshPresets,
17370
+ options,
17371
+ busy,
17372
+ error
17373
+ };
17374
+ }
17375
+ //#endregion
17376
+ //#region src/composites/ptz-overlay.tsx
17377
+ /**
17378
+ * PTZOverlay — pan / tilt / zoom controls.
17379
+ *
17380
+ * Two visual variants driven by `mode`:
17381
+ * - `'overlay'` (default): translucent dark pill positioned bottom-
17382
+ * right of the camera viewport. Used as `extraOverlay` on the
17383
+ * `CameraStreamPlayer` so operators can drive PTZ without leaving
17384
+ * the live view. Opaque background + subtle ring keeps the d-pad
17385
+ * legible against any frame.
17386
+ * - `'panel'`: full-bleed inside a host container (e.g. the floating
17387
+ * PTZ panel in DeviceDetail). No absolute positioning, no inner
17388
+ * wrapper card — the host's chrome is the only frame. Inherits the
17389
+ * surrounding `bg-surface` so the dark theme reads consistently
17390
+ * instead of the previous always-dark-bubble look.
17391
+ *
17392
+ * Interaction model is identical across modes: short tap fires a
17393
+ * discrete pulse (`move`); long press starts continuous motion until
17394
+ * release (`startContinuous` + `stopContinuous` on pointer up).
17395
+ */
17396
+ function DPadButton({ direction, icon: Icon, disabled, className, variant, onMove, onStart, onStop }) {
17397
+ const [pressedAt, setPressedAt] = (0, react$1.useState)(null);
17398
+ const [continuous, setContinuous] = (0, react$1.useState)(false);
17399
+ const handlePointerDown = (0, react$1.useCallback)((e) => {
17400
+ if (disabled) return;
17401
+ e.currentTarget.setPointerCapture(e.pointerId);
17402
+ setPressedAt(Date.now());
17403
+ setContinuous(false);
17404
+ const timer = setTimeout(() => {
17405
+ setContinuous(true);
17406
+ onStart(direction);
17407
+ }, 250);
17408
+ e.currentTarget.dataset.timer = String(timer);
17409
+ }, [
17410
+ direction,
17411
+ disabled,
17412
+ onStart
17413
+ ]);
17414
+ const handlePointerUp = (0, react$1.useCallback)((e) => {
17415
+ const timerId = Number(e.currentTarget.dataset.timer);
17416
+ if (timerId) clearTimeout(timerId);
17417
+ e.currentTarget.dataset.timer = "";
17418
+ if (continuous) onStop();
17419
+ else if (pressedAt !== null) onMove(direction);
17420
+ setPressedAt(null);
17421
+ setContinuous(false);
17422
+ }, [
17423
+ continuous,
17424
+ direction,
17425
+ onMove,
17426
+ onStop,
17427
+ pressedAt
17428
+ ]);
17429
+ const handlePointerCancel = (0, react$1.useCallback)((e) => {
17430
+ const timerId = Number(e.currentTarget.dataset.timer);
17431
+ if (timerId) clearTimeout(timerId);
17432
+ e.currentTarget.dataset.timer = "";
17433
+ if (continuous) onStop();
17434
+ setPressedAt(null);
17435
+ setContinuous(false);
17436
+ }, [continuous, onStop]);
17437
+ const sizeClass = variant === "panel" ? "h-9 w-9" : "h-7 w-7";
17438
+ const iconSizeClass = variant === "panel" ? "h-4 w-4" : "h-3.5 w-3.5";
17439
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
17440
+ type: "button",
17441
+ disabled,
17442
+ onPointerDown: handlePointerDown,
17443
+ onPointerUp: handlePointerUp,
17444
+ onPointerCancel: handlePointerCancel,
17445
+ className: cn("flex items-center justify-center rounded transition-colors disabled:opacity-40", variant === "panel" ? "text-foreground hover:bg-surface-hover active:bg-surface-hover/80" : "text-white hover:bg-white/15 active:bg-white/25", sizeClass, className),
17446
+ title: direction,
17447
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { className: iconSizeClass })
17448
+ });
17449
+ }
17450
+ function PTZOverlay({ controls, mode = "overlay", showPresets, showZoom = true, showHome = true, className }) {
17451
+ const { move, startContinuous, stopContinuous, zoom, goHome, goToPreset, savePreset, deletePreset, presets, options, busy, error } = controls;
17452
+ const confirm = useConfirm();
17453
+ const [presetsOpen, setPresetsOpen] = (0, react$1.useState)(false);
17454
+ const [presetName, setPresetName] = (0, react$1.useState)("");
17455
+ const presetsVisible = (showPresets ?? presets.length > 0) && presets.length > 0;
17456
+ const isPanel = mode === "panel";
17457
+ const presetManagementVisible = isPanel && (options?.supportsPresets ?? false);
17458
+ const maxPresetsReached = options?.maxPresets !== void 0 && presets.length >= options.maxPresets;
17459
+ const handleSavePreset = (0, react$1.useCallback)(async () => {
17460
+ const name = presetName.trim();
17461
+ if (!name || busy || maxPresetsReached) return;
17462
+ if (await savePreset(name) !== void 0) setPresetName("");
17463
+ }, [
17464
+ presetName,
17465
+ busy,
17466
+ maxPresetsReached,
17467
+ savePreset
17468
+ ]);
17469
+ const handleDeletePreset = (0, react$1.useCallback)(async (presetId, label) => {
17470
+ if (!await confirm({
17471
+ title: "Delete preset",
17472
+ message: `Delete PTZ preset "${label}"? This removes it from the camera.`,
17473
+ confirmLabel: "Delete",
17474
+ variant: "danger"
17475
+ })) return;
17476
+ await deletePreset(presetId);
17477
+ }, [confirm, deletePreset]);
17478
+ const containerClass = isPanel ? "flex flex-col items-stretch gap-2 w-full h-full p-3" : "pointer-events-auto absolute top-3 right-3 z-10 flex flex-col items-end gap-2";
17479
+ const rowClass = isPanel ? cn("flex items-center gap-3 rounded-lg p-2", busy && "ring-1 ring-primary/40") : cn("flex items-center gap-2 rounded-lg border border-white/30 bg-zinc-900/90 backdrop-blur-md p-2 shadow-lg shadow-black/40", busy && "ring-1 ring-primary/40");
17480
+ const sideButtonSize = isPanel ? "h-9 w-9" : "h-7 w-7";
17481
+ const sideIconSize = isPanel ? "h-4 w-4" : "h-3.5 w-3.5";
17482
+ const sideButtonHover = isPanel ? "text-foreground hover:bg-surface-hover" : "text-white hover:bg-white/15";
17483
+ const sepClass = isPanel ? "h-12 w-px bg-border mx-1" : "h-12 w-px bg-white/15 mx-1";
17484
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
17485
+ className: cn(containerClass, className),
17486
+ children: [
17487
+ error && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
17488
+ className: "rounded bg-danger/90 px-2 py-1 text-[10px] font-medium text-white shadow-lg max-w-[200px] self-center",
17489
+ children: ["PTZ: ", error]
17490
+ }),
17491
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
17492
+ className: cn(rowClass, isPanel && "justify-center"),
17493
+ children: [
17494
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
17495
+ className: "grid grid-cols-3 gap-0.5",
17496
+ children: [
17497
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
17498
+ "aria-hidden": true,
17499
+ className: isPanel ? "h-9 w-9" : "h-7 w-7"
17500
+ }),
17501
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DPadButton, {
17502
+ direction: "up",
17503
+ icon: ArrowUp,
17504
+ variant: mode,
17505
+ onMove: move,
17506
+ onStart: startContinuous,
17507
+ onStop: stopContinuous
17508
+ }),
17509
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
17510
+ "aria-hidden": true,
17511
+ className: isPanel ? "h-9 w-9" : "h-7 w-7"
17512
+ }),
17513
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DPadButton, {
17514
+ direction: "left",
17515
+ icon: ArrowLeft,
17516
+ variant: mode,
17517
+ onMove: move,
17518
+ onStart: startContinuous,
17519
+ onStop: stopContinuous
17520
+ }),
17521
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
17522
+ "aria-hidden": true,
17523
+ className: isPanel ? "h-9 w-9" : "h-7 w-7"
17524
+ }),
17525
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DPadButton, {
17526
+ direction: "right",
17527
+ icon: ArrowRight,
17528
+ variant: mode,
17529
+ onMove: move,
17530
+ onStart: startContinuous,
17531
+ onStop: stopContinuous
17532
+ }),
17533
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
17534
+ "aria-hidden": true,
17535
+ className: isPanel ? "h-9 w-9" : "h-7 w-7"
17536
+ }),
17537
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DPadButton, {
17538
+ direction: "down",
17539
+ icon: ArrowDown,
17540
+ variant: mode,
17541
+ onMove: move,
17542
+ onStart: startContinuous,
17543
+ onStop: stopContinuous
17544
+ }),
17545
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
17546
+ "aria-hidden": true,
17547
+ className: isPanel ? "h-9 w-9" : "h-7 w-7"
17548
+ })
17549
+ ]
17550
+ }),
17551
+ (showZoom || showHome) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: sepClass }),
17552
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
17553
+ className: "flex flex-col gap-0.5",
17554
+ children: [showZoom && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
17555
+ type: "button",
17556
+ onClick: () => zoom("in"),
17557
+ disabled: busy,
17558
+ title: "Zoom in",
17559
+ className: cn("flex items-center justify-center rounded disabled:opacity-40", sideButtonSize, sideButtonHover),
17560
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZoomIn, { className: sideIconSize })
17561
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
17562
+ type: "button",
17563
+ onClick: () => zoom("out"),
17564
+ disabled: busy,
17565
+ title: "Zoom out",
17566
+ className: cn("flex items-center justify-center rounded disabled:opacity-40", sideButtonSize, sideButtonHover),
17567
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZoomOut, { className: sideIconSize })
17568
+ })] }), showHome && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
17569
+ type: "button",
17570
+ onClick: () => goHome(),
17571
+ disabled: busy,
17572
+ title: "Go home",
17573
+ className: cn("flex items-center justify-center rounded disabled:opacity-40", sideButtonSize, sideButtonHover),
17574
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(House, { className: sideIconSize })
17575
+ })]
17576
+ }),
17577
+ presetsVisible && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
17578
+ className: "relative",
17579
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
17580
+ type: "button",
17581
+ onClick: () => setPresetsOpen((v) => !v),
17582
+ disabled: busy,
17583
+ className: cn("flex items-center gap-1 rounded px-2 text-[10px] disabled:opacity-40", isPanel ? "h-9 text-foreground hover:bg-surface-hover" : "h-7 text-white hover:bg-white/15"),
17584
+ title: "Presets",
17585
+ children: ["Presets", /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronDown, { className: cn("h-3 w-3 transition-transform", presetsOpen && "rotate-180") })]
17586
+ }), presetsOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
17587
+ className: cn("absolute right-0 top-full mt-1 min-w-[140px] rounded-lg shadow-xl py-1 z-20", isPanel ? "bg-surface border border-border" : "border border-white/30 bg-zinc-900/95 backdrop-blur-md"),
17588
+ children: presets.map((p) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
17589
+ className: cn("group flex items-center", isPanel ? "hover:bg-surface-hover" : "hover:bg-white/15"),
17590
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
17591
+ type: "button",
17592
+ onClick: () => {
17593
+ goToPreset(p.id);
17594
+ setPresetsOpen(false);
17595
+ },
17596
+ disabled: busy,
17597
+ className: cn("flex-1 px-3 py-1.5 text-left text-[10px] disabled:opacity-40", isPanel ? "text-foreground" : "text-white"),
17598
+ children: p.name || p.id
17599
+ }), presetManagementVisible && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
17600
+ type: "button",
17601
+ onClick: () => void handleDeletePreset(p.id, p.name || p.id),
17602
+ disabled: busy,
17603
+ title: `Delete preset ${p.name || p.id}`,
17604
+ className: cn("mr-1 flex h-5 w-5 shrink-0 items-center justify-center rounded", "text-foreground-subtle hover:bg-danger/15 hover:text-danger disabled:opacity-40"),
17605
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(X, { className: "h-3 w-3" })
17606
+ })]
17607
+ }, p.id))
17608
+ })]
17609
+ })
17610
+ ]
17611
+ }),
17612
+ presetManagementVisible && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
17613
+ className: "flex flex-col gap-1 rounded-lg border border-border p-2",
17614
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
17615
+ className: "flex items-center gap-2",
17616
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
17617
+ type: "text",
17618
+ value: presetName,
17619
+ onChange: (e) => setPresetName(e.target.value),
17620
+ onKeyDown: (e) => {
17621
+ if (e.key === "Enter") handleSavePreset();
17622
+ },
17623
+ disabled: busy || maxPresetsReached,
17624
+ placeholder: "New preset name",
17625
+ maxLength: 64,
17626
+ className: cn("min-w-0 flex-1 rounded border border-border bg-surface px-2 py-1 text-[11px] text-foreground", "placeholder:text-foreground-subtle focus:outline-none focus:ring-1 focus:ring-primary/50", "disabled:opacity-40")
17627
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
17628
+ type: "button",
17629
+ onClick: () => void handleSavePreset(),
17630
+ disabled: busy || maxPresetsReached || presetName.trim().length === 0,
17631
+ title: maxPresetsReached ? `Camera preset limit reached (${options?.maxPresets})` : "Save current position as a new preset",
17632
+ className: cn("flex h-7 shrink-0 items-center gap-1 rounded px-2 text-[10px] font-medium", "bg-primary/15 text-primary hover:bg-primary/25 disabled:opacity-40"),
17633
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Plus, { className: "h-3 w-3" }), "Save current position"]
17634
+ })]
17635
+ }), maxPresetsReached && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
17636
+ className: "text-[10px] text-foreground-subtle",
17637
+ children: [
17638
+ "Camera preset limit reached (",
17639
+ options?.maxPresets,
17640
+ "). Delete a preset to add a new one."
17641
+ ]
17642
+ })]
17643
+ })
17644
+ ]
17645
+ });
17646
+ }
17647
+ //#endregion
17648
+ //#region src/composites/cap-settings/PtzPanel.tsx
17649
+ /**
17650
+ * PtzPanel — live pan/tilt/zoom controls for the `ptz` capability. A
17651
+ * cap-settings component (ui-library), mounted as a cap-contributed
17652
+ * top-level device-detail tab via the cap-UI contribution mechanism
17653
+ * (the `ptz` cap declares `ui: { tab: 'ptz', kind: 'static', ... }`).
17654
+ *
17655
+ * Consolidates the former admin-ui `PTZPanelContent`: `usePTZ` drives
17656
+ * the controls, `PTZOverlay` renders them (`mode='panel'`), and an
17657
+ * Autofocus toggle is shown only when `getOptions().hasAutofocus`.
17658
+ * Autotrack is its own cap-UI contribution (`ptz-autotrack`), mounted
17659
+ * independently by the contribution mechanism — not nested here.
17660
+ */
17661
+ /** Autofocus toggle — only mounted when `hasAutofocus`. Reads the cap's
17662
+ * `getStatus().autofocus` and drives `setAutofocus`. */
17663
+ function AutofocusToggle({ deviceId }) {
17664
+ const dev = useDeviceProxy(useSystem().trpcClient, deviceId);
17665
+ const [enabled, setEnabled] = (0, react$1.useState)(null);
17666
+ const [busy, setBusy] = (0, react$1.useState)(false);
17667
+ (0, react$1.useEffect)(() => {
17668
+ if (!dev) return void 0;
17669
+ let cancelled = false;
17670
+ (async () => {
17671
+ try {
17672
+ const status = await dev.ptz?.getStatus({});
17673
+ if (cancelled || !status) return;
17674
+ setEnabled(status.autofocus);
17675
+ } catch {}
17676
+ })();
17677
+ return () => {
17678
+ cancelled = true;
17679
+ };
17680
+ }, [dev]);
17681
+ const toggle = (0, react$1.useCallback)(async () => {
17682
+ if (!dev?.ptz || enabled === null) return;
17683
+ setBusy(true);
17684
+ try {
17685
+ const next = !enabled;
17686
+ await dev.ptz.setAutofocus({ enabled: next });
17687
+ setEnabled(next);
17688
+ } catch (err) {
17689
+ console.error("ptz.setAutofocus failed", err);
17690
+ } finally {
17691
+ setBusy(false);
17692
+ }
17693
+ }, [dev, enabled]);
17694
+ if (enabled === null) return null;
17695
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
17696
+ className: "flex items-center justify-between border-t border-border/40 mt-2 pt-2",
17697
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
17698
+ className: "text-[10.5px] font-semibold text-foreground",
17699
+ children: "Autofocus"
17700
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
17701
+ type: "button",
17702
+ onClick: () => void toggle(),
17703
+ disabled: busy,
17704
+ "aria-pressed": enabled,
17705
+ className: `flex items-center gap-1.5 rounded-md px-2 py-1 text-[10px] font-medium transition-colors ${enabled ? "bg-success/15 text-success hover:bg-success/25" : "bg-surface-elevated/40 text-foreground-subtle hover:bg-surface-hover hover:text-foreground"} disabled:opacity-50`,
17706
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: `h-1.5 w-1.5 rounded-full ${enabled ? "bg-success" : "bg-foreground-subtle/40"}` }), enabled ? "On" : "Off"]
17707
+ })]
17708
+ });
17709
+ }
17710
+ function PtzPanel({ deviceId }) {
17711
+ const system = useSystem();
17712
+ const ready = Number.isFinite(deviceId);
17713
+ const controls = usePTZ(system.trpcClient, ready ? deviceId : 0, { enabled: ready });
17714
+ if (!ready) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
17715
+ className: "flex items-center justify-center h-full text-[11px] text-foreground-subtle italic",
17716
+ children: [
17717
+ "Device #",
17718
+ deviceId,
17719
+ " not loaded."
17720
+ ]
17721
+ });
17722
+ const hasAutofocus = controls.options?.hasAutofocus === true;
17723
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
17724
+ className: "flex flex-col h-full overflow-y-auto",
17725
+ children: [
17726
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
17727
+ className: "text-[11px] font-semibold text-foreground-subtle uppercase tracking-wider mb-2",
17728
+ children: "PTZ"
17729
+ }),
17730
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PTZOverlay, {
17731
+ controls,
17732
+ mode: "panel"
17733
+ }),
17734
+ hasAutofocus && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AutofocusToggle, { deviceId })
17735
+ ]
17736
+ });
17737
+ }
17738
+ //#endregion
17739
+ //#region src/hooks/use-device-autotrack.ts
17740
+ /**
17741
+ * `useDeviceAutotrack` — typed wrapper around the `ptz-autotrack` cap.
17742
+ *
17743
+ * Surface:
17744
+ * - `status` — current `{enabled, lastChangedAt, currentSettings}`,
17745
+ * polled at 5s intervals while the hook is mounted.
17746
+ * - `setEnabled(on)` — flip the on/off state.
17747
+ * - `setSettings(partial)` — patch one or more settings keys.
17748
+ * - `isPending` — true while a mutation is in flight.
17749
+ *
17750
+ * Returns `null` for `status` when the cap isn't available (device
17751
+ * doesn't support autotrack) — callers gate render on this OR on the
17752
+ * device's `PtzAutotrack` feature flag.
17753
+ */
17754
+ function useDeviceAutotrack(deviceId) {
17755
+ const queryClient = (0, _tanstack_react_query.useQueryClient)();
17756
+ const statusQuery = usePtzAutotrackGetStatus({ deviceId: deviceId ?? 0 }, {
17757
+ enabled: deviceId !== null && Number.isFinite(deviceId),
17758
+ refetchInterval: 5e3,
17759
+ retry: 1
17760
+ });
17761
+ const setEnabledMutation = usePtzAutotrackSetEnabled({ onSuccess: () => {
17762
+ queryClient.invalidateQueries({ queryKey: [["ptzAutotrack"]] });
17763
+ } });
17764
+ const setSettingsMutation = usePtzAutotrackSetSettings({ onSuccess: () => {
17765
+ queryClient.invalidateQueries({ queryKey: [["ptzAutotrack"]] });
17766
+ } });
17767
+ const setEnabled = (0, react$1.useCallback)(async (on) => {
17768
+ if (deviceId === null) return;
17769
+ await setEnabledMutation.mutateAsync({
17770
+ deviceId,
17771
+ enabled: on
17772
+ });
17773
+ }, [deviceId, setEnabledMutation]);
17774
+ const setSettings = (0, react$1.useCallback)(async (patch) => {
17775
+ if (deviceId === null) return;
17776
+ await setSettingsMutation.mutateAsync({
17777
+ deviceId,
17778
+ settings: patch
17779
+ });
17780
+ }, [deviceId, setSettingsMutation]);
17781
+ const errorMsg = (() => {
17782
+ if (statusQuery.error) return statusQuery.error.message;
17783
+ if (setEnabledMutation.error) return setEnabledMutation.error.message;
17784
+ if (setSettingsMutation.error) return setSettingsMutation.error.message;
17785
+ return null;
17786
+ })();
17787
+ return {
17788
+ status: statusQuery.data ?? null,
17789
+ isLoading: statusQuery.isLoading,
17790
+ isPending: setEnabledMutation.isPending || setSettingsMutation.isPending,
17791
+ error: errorMsg,
17792
+ setEnabled,
17793
+ setSettings
17794
+ };
17795
+ }
17796
+ //#endregion
17797
+ //#region src/composites/cap-settings/AutotrackSection.tsx
17798
+ /**
17799
+ * Autotrack settings card — rendered inside the PTZ panel when the
17800
+ * device has `DeviceFeature.PtzAutotrack`. Three knobs (cross-vendor
17801
+ * schema): target type, stop delay, disappear delay, plus an enable /
17802
+ * disable toggle that drives the `ptz-autotrack` cap directly.
17803
+ *
17804
+ * Save semantics:
17805
+ * - Toggle (enable/disable) calls `setEnabled` immediately.
17806
+ * - Form fields debounce-save on blur — no explicit Save button.
17807
+ *
17808
+ * Vendor capability hints:
17809
+ * - The cap's status carries `currentSettings` from the camera's
17810
+ * own GET; rendered on every poll so the operator sees what's
17811
+ * really applied vs what they typed.
17812
+ * - When the firmware ignored a setting, the field stays editable.
17813
+ */
17814
+ function AutotrackSection({ deviceId }) {
17815
+ const { status, isLoading, isPending, error, setEnabled, setSettings } = useDeviceAutotrack(Number.isFinite(deviceId) ? deviceId : null);
17816
+ const [draft, setDraft] = (0, react$1.useState)({
17817
+ targetType: "",
17818
+ stopDelaySeconds: 30,
17819
+ disappearDelaySeconds: 15
17820
+ });
17821
+ const [editing, setEditing] = (0, react$1.useState)(false);
17822
+ (0, react$1.useEffect)(() => {
17823
+ if (editing) return;
17824
+ const s = status?.currentSettings;
17825
+ if (!s) return;
17826
+ setDraft({
17827
+ targetType: s.targetType,
17828
+ stopDelaySeconds: s.stopDelaySeconds,
17829
+ disappearDelaySeconds: s.disappearDelaySeconds
17830
+ });
17831
+ }, [status, editing]);
17832
+ if (isLoading && !status) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
17833
+ className: "flex items-center gap-2 px-3 py-2 text-[10.5px] text-foreground-subtle",
17834
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LoaderCircle, { className: "h-3 w-3 animate-spin" }), "Loading autotrack…"]
17835
+ });
17836
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
17837
+ className: "border-t border-border/40 mt-2 pt-2 px-1 space-y-2",
17838
+ children: [
17839
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
17840
+ className: "flex items-center justify-between px-2",
17841
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("h3", {
17842
+ className: "flex items-center gap-1.5 text-[11px] font-semibold text-foreground-subtle uppercase tracking-wider",
17843
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Crosshair, { className: "h-3 w-3 text-primary" }), "Autotrack"]
17844
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
17845
+ type: "button",
17846
+ onClick: () => {
17847
+ setEnabled(!(status?.enabled ?? false));
17848
+ },
17849
+ disabled: isPending,
17850
+ className: `flex items-center gap-1.5 rounded-md px-2 py-1 text-[10px] font-medium transition-colors ${status?.enabled ? "bg-success/15 text-success hover:bg-success/25" : "bg-surface-elevated/40 text-foreground-subtle hover:bg-surface-hover hover:text-foreground"} disabled:opacity-50`,
17851
+ "aria-pressed": status?.enabled ?? false,
17852
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: `h-1.5 w-1.5 rounded-full ${status?.enabled ? "bg-success" : "bg-foreground-subtle/40"}` }), status?.enabled ? "Enabled" : "Disabled"]
17853
+ })]
17854
+ }),
17855
+ error && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
17856
+ className: "flex items-start gap-1.5 rounded-md border border-danger/30 bg-danger/5 px-2 py-1.5 text-[10px] text-danger",
17857
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CircleAlert, { className: "h-3 w-3 flex-shrink-0 mt-0.5" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
17858
+ className: "leading-snug break-words",
17859
+ children: error
17860
+ })]
17861
+ }),
17862
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
17863
+ className: "grid grid-cols-2 gap-2 px-2",
17864
+ children: [
17865
+ (status?.supportedTargetTypes?.length ?? 0) > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
17866
+ className: "col-span-2 flex flex-col gap-1",
17867
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
17868
+ className: "text-[10px] font-medium text-foreground-subtle",
17869
+ children: "Target type"
17870
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
17871
+ value: draft.targetType,
17872
+ onFocus: () => setEditing(true),
17873
+ onChange: (e) => setDraft((d) => ({
17874
+ ...d,
17875
+ targetType: e.target.value
17876
+ })),
17877
+ onBlur: () => {
17878
+ setEditing(false);
17879
+ const current = status?.currentSettings?.targetType ?? "";
17880
+ if (draft.targetType !== current) setSettings({ targetType: draft.targetType });
17881
+ },
17882
+ disabled: isPending,
17883
+ className: "rounded-md border border-border bg-surface px-2 py-1 text-[11px] text-foreground focus:border-primary focus:outline-none disabled:opacity-50",
17884
+ children: (status?.supportedTargetTypes ?? []).map((opt) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
17885
+ value: opt.value,
17886
+ children: opt.label
17887
+ }, opt.value))
17888
+ })]
17889
+ }),
17890
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
17891
+ className: "flex flex-col gap-1",
17892
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
17893
+ className: "text-[10px] font-medium text-foreground-subtle",
17894
+ children: "Stop delay (s)"
17895
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
17896
+ type: "number",
17897
+ min: 0,
17898
+ max: 300,
17899
+ value: draft.stopDelaySeconds,
17900
+ onFocus: () => setEditing(true),
17901
+ onChange: (e) => setDraft((d) => ({
17902
+ ...d,
17903
+ stopDelaySeconds: Number(e.target.value) || 0
17904
+ })),
17905
+ onBlur: () => {
17906
+ setEditing(false);
17907
+ const current = status?.currentSettings?.stopDelaySeconds ?? 30;
17908
+ if (draft.stopDelaySeconds !== current) setSettings({ stopDelaySeconds: draft.stopDelaySeconds });
17909
+ },
17910
+ disabled: isPending,
17911
+ className: "rounded-md border border-border bg-surface px-2 py-1 text-[11px] text-foreground focus:border-primary focus:outline-none disabled:opacity-50"
17912
+ })]
17913
+ }),
17914
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
17915
+ className: "flex flex-col gap-1",
17916
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
17917
+ className: "text-[10px] font-medium text-foreground-subtle",
17918
+ children: "Disappear delay (s)"
17919
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
17920
+ type: "number",
17921
+ min: 0,
17922
+ max: 300,
17923
+ value: draft.disappearDelaySeconds,
17924
+ onFocus: () => setEditing(true),
17925
+ onChange: (e) => setDraft((d) => ({
17926
+ ...d,
17927
+ disappearDelaySeconds: Number(e.target.value) || 0
17928
+ })),
17929
+ onBlur: () => {
17930
+ setEditing(false);
17931
+ const current = status?.currentSettings?.disappearDelaySeconds ?? 15;
17932
+ if (draft.disappearDelaySeconds !== current) setSettings({ disappearDelaySeconds: draft.disappearDelaySeconds });
17933
+ },
17934
+ disabled: isPending,
17935
+ className: "rounded-md border border-border bg-surface px-2 py-1 text-[11px] text-foreground focus:border-primary focus:outline-none disabled:opacity-50"
17936
+ })]
17937
+ })
17938
+ ]
17939
+ }),
17940
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
17941
+ className: "px-2 text-[9.5px] text-foreground-subtle leading-snug italic",
17942
+ children: "Settings save on blur. Vendor support varies — Reolink honours all three; Hikvision honours target type + stop delay only (disappear delay is accepted but ignored at firmware push)."
17943
+ })
17944
+ ]
17945
+ });
17946
+ }
17947
+ //#endregion
17948
+ //#region src/contexts/player-overlays.tsx
17949
+ /**
17950
+ * Player overlay registry — pluggable layers + toolbar buttons for
17951
+ * the device-detail live-frame `StreamPanel`.
17952
+ *
17953
+ * Why a registry: previously `StreamPanelContent` hardcoded each
17954
+ * overlay (PTZ, intercom toggle, zone editor) and threaded the
17955
+ * editing state by hand. Adding a new feature (audio waveform,
17956
+ * detection bbox tracker, recording controls, …) meant another
17957
+ * round of plumbing through the host file and StreamPanel's prop
17958
+ * surface. The registry lets a sibling component (Detection tab,
17959
+ * Live Stats tab, an addon-page extension) declare its own layer
17960
+ * imperatively via a hook, and the host renders whatever's
17961
+ * registered.
17962
+ *
17963
+ * Two registries live side-by-side:
17964
+ *
17965
+ * 1. **layers** — absolute-positioned React nodes drawn over the
17966
+ * video frame (zones polygon canvas, motion bbox overlay,
17967
+ * audio waveform). Rendered ordered by `order` (lower first
17968
+ * → bottom; higher → top); the last-registered layer wins
17969
+ * ties.
17970
+ *
17971
+ * 2. **toolbar buttons** — controls surfaced in the player's
17972
+ * always-visible toolbar cluster (next to Intercom + Play/Stop).
17973
+ * Each carries an icon, label, controlled `active` flag, and
17974
+ * a click handler. The host (StreamPanel) renders them
17975
+ * uniformly; tone variants stay simple (`'default' | 'primary'`).
17976
+ *
17977
+ * Lifecycle: hooks register their layer / button on mount and
17978
+ * unregister on unmount, so swapping tabs (e.g. leaving Detection)
17979
+ * automatically tears down the registration. Re-registering with
17980
+ * the same `id` replaces the prior entry — meaning a single hook
17981
+ * call can update its overlay/button props on every render without
17982
+ * leaking entries.
17983
+ *
17984
+ * Provider scope: typically wraps the whole device-detail subtree
17985
+ * so the StreamPanel + every tab share the same registry. One
17986
+ * provider per `deviceId`; switching device IDs unmounts the
17987
+ * provider naturally.
17988
+ */
17989
+ var PlayerOverlaysStateContext = createSharedContext("camstack:player-overlays-state", null);
17990
+ var PlayerOverlaysActionsContext = createSharedContext("camstack:player-overlays-actions", null);
17991
+ function PlayerOverlaysProvider({ children }) {
17992
+ const [layers, setLayers] = (0, react$1.useState)(() => /* @__PURE__ */ new Map());
17993
+ const [buttons, setButtons] = (0, react$1.useState)(() => /* @__PURE__ */ new Map());
17994
+ const setLayer = (0, react$1.useCallback)((layer) => {
17995
+ setLayers((prev) => {
17996
+ const next = new Map(prev);
17997
+ next.set(layer.id, layer);
17998
+ return next;
17999
+ });
18000
+ }, []);
18001
+ const removeLayer = (0, react$1.useCallback)((id) => {
18002
+ setLayers((prev) => {
18003
+ if (!prev.has(id)) return prev;
18004
+ const next = new Map(prev);
18005
+ next.delete(id);
18006
+ return next;
18007
+ });
18008
+ }, []);
18009
+ const setButton = (0, react$1.useCallback)((button) => {
18010
+ setButtons((prev) => {
18011
+ const next = new Map(prev);
18012
+ next.set(button.id, button);
18013
+ return next;
18014
+ });
18015
+ }, []);
18016
+ const removeButton = (0, react$1.useCallback)((id) => {
18017
+ setButtons((prev) => {
18018
+ if (!prev.has(id)) return prev;
18019
+ const next = new Map(prev);
18020
+ next.delete(id);
18021
+ return next;
18022
+ });
18023
+ }, []);
18024
+ const stateValue = (0, react$1.useMemo)(() => ({
18025
+ layers,
18026
+ buttons
18027
+ }), [layers, buttons]);
18028
+ const actionsValue = (0, react$1.useMemo)(() => ({
18029
+ setLayer,
18030
+ removeLayer,
18031
+ setButton,
18032
+ removeButton
18033
+ }), [
18034
+ setLayer,
18035
+ removeLayer,
18036
+ setButton,
18037
+ removeButton
18038
+ ]);
18039
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlayerOverlaysStateContext.Provider, {
18040
+ value: stateValue,
18041
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlayerOverlaysActionsContext.Provider, {
18042
+ value: actionsValue,
18043
+ children
18044
+ })
18045
+ });
18046
+ }
18047
+ /** Snapshot of registered layers ordered by `order` (asc, ties resolved
18048
+ * by insertion order of the underlying Map). Returns `[]` outside a
18049
+ * provider. */
18050
+ function usePlayerOverlayLayers() {
18051
+ const state = (0, react$1.useContext)(PlayerOverlaysStateContext);
18052
+ return (0, react$1.useMemo)(() => {
18053
+ if (!state) return [];
18054
+ return [...state.layers.values()].sort((a, b) => a.order - b.order);
18055
+ }, [state]);
18056
+ }
18057
+ /** Snapshot of registered toolbar buttons, ordered by `order` (asc). */
18058
+ function usePlayerToolbarButtons() {
18059
+ const state = (0, react$1.useContext)(PlayerOverlaysStateContext);
18060
+ return (0, react$1.useMemo)(() => {
18061
+ if (!state) return [];
18062
+ return [...state.buttons.values()].sort((a, b) => a.order - b.order);
18063
+ }, [state]);
18064
+ }
18065
+ /**
18066
+ * Register an overlay layer for the lifetime of the calling component.
18067
+ * Re-registers on every render with the latest spec; auto-unregisters
18068
+ * on unmount. Pass `null` to skip registration when the layer is
18069
+ * conditionally enabled (the hook still runs every render — keeps
18070
+ * react-hook order stable across spec === null toggles).
18071
+ *
18072
+ * Callers that build the spec inline should memoise it (`useMemo`)
18073
+ * to avoid re-registering on every parent render — context-write
18074
+ * effects depend on referential equality of the spec object.
18075
+ */
18076
+ function usePlayerOverlayLayer(spec) {
18077
+ const actions = (0, react$1.useContext)(PlayerOverlaysActionsContext);
18078
+ (0, react$1.useEffect)(() => {
18079
+ if (!actions || !spec) return void 0;
18080
+ actions.setLayer(spec);
18081
+ return () => actions.removeLayer(spec.id);
18082
+ }, [actions, spec]);
18083
+ }
18084
+ /** Same shape as `usePlayerOverlayLayer`, scoped to toolbar buttons. */
18085
+ function usePlayerToolbarButton(spec) {
18086
+ const actions = (0, react$1.useContext)(PlayerOverlaysActionsContext);
18087
+ (0, react$1.useEffect)(() => {
18088
+ if (!actions || !spec) return void 0;
18089
+ actions.setButton(spec);
18090
+ return () => actions.removeButton(spec.id);
18091
+ }, [actions, spec]);
18092
+ }
18093
+ //#endregion
18094
+ //#region src/composites/cap-settings/MotionGridCanvas.tsx
18095
+ /**
18096
+ * MotionGridCanvas — the on-frame motion-zone grid editor.
18097
+ *
18098
+ * This component is *only* the editable lattice painted over the live
18099
+ * frame: a plain CSS-grid of `gridWidth × gridHeight` cells. It carries
18100
+ * NO controls — enabled / sensitivity / save / quick-actions all live
18101
+ * in the management bar inside the settings section (`MotionZonesTab`),
18102
+ * so the live frame stays unobstructed.
18103
+ *
18104
+ * Interaction:
18105
+ * - Click a cell → toggle it.
18106
+ * - Press + drag → paint every touched cell to whatever the
18107
+ * first-touched cell flipped TO (the standard grid-mask gesture).
18108
+ *
18109
+ * Purely controlled — every change is forwarded through `onCellsChange`;
18110
+ * the owning `MotionZonesTab` keeps the single source of truth.
18111
+ */
18112
+ function MotionGridCanvas({ options, cells, onCellsChange }) {
18113
+ const { gridWidth, gridHeight } = options;
18114
+ const total = gridWidth * gridHeight;
18115
+ const paintingRef = (0, react$1.useRef)(false);
18116
+ const paintValueRef = (0, react$1.useRef)(true);
18117
+ const paintedRef = (0, react$1.useRef)(/* @__PURE__ */ new Set());
18118
+ const [, forceTick] = (0, react$1.useState)(0);
18119
+ const applyCell = (0, react$1.useCallback)((index, value) => {
18120
+ if (index < 0 || index >= total) return;
18121
+ if (cells[index] === value) return;
18122
+ const next = [...cells];
18123
+ while (next.length < total) next.push(false);
18124
+ next.length = total;
18125
+ next[index] = value;
18126
+ onCellsChange(next);
18127
+ }, [
18128
+ cells,
18129
+ total,
18130
+ onCellsChange
18131
+ ]);
18132
+ const handlePointerDown = (0, react$1.useCallback)((index) => (e) => {
18133
+ e.preventDefault();
18134
+ const nextValue = !cells[index];
18135
+ paintingRef.current = true;
18136
+ paintValueRef.current = nextValue;
18137
+ paintedRef.current = new Set([index]);
18138
+ applyCell(index, nextValue);
18139
+ forceTick((t) => t + 1);
18140
+ }, [cells, applyCell]);
18141
+ const handlePointerEnter = (0, react$1.useCallback)((index) => () => {
18142
+ if (!paintingRef.current) return;
18143
+ if (paintedRef.current.has(index)) return;
18144
+ paintedRef.current.add(index);
18145
+ applyCell(index, paintValueRef.current);
18146
+ }, [applyCell]);
18147
+ const endPaint = (0, react$1.useCallback)(() => {
18148
+ if (!paintingRef.current) return;
18149
+ paintingRef.current = false;
18150
+ paintedRef.current = /* @__PURE__ */ new Set();
18151
+ }, []);
18152
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
18153
+ className: "absolute inset-0 select-none grid",
18154
+ style: {
18155
+ gridTemplateColumns: `repeat(${String(gridWidth)}, 1fr)`,
18156
+ gridTemplateRows: `repeat(${String(gridHeight)}, 1fr)`
18157
+ },
18158
+ onPointerUp: endPaint,
18159
+ onPointerLeave: endPaint,
18160
+ children: Array.from({ length: total }, (_, i) => {
18161
+ const active = cells[i] === true;
18162
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
18163
+ type: "button",
18164
+ "aria-label": `motion cell ${String(i)}`,
18165
+ "aria-pressed": active,
18166
+ onPointerDown: handlePointerDown(i),
18167
+ onPointerEnter: handlePointerEnter(i),
18168
+ className: active ? "border border-primary/70 bg-primary/55 hover:bg-primary/65 transition-colors" : "border border-white/25 bg-white/10 hover:bg-white/20 transition-colors"
18169
+ }, i);
18170
+ })
18171
+ });
18172
+ }
18173
+ //#endregion
18174
+ //#region src/composites/cap-settings/MotionZonesSettings.tsx
18175
+ /**
18176
+ * MotionZonesSettings — management surface for the on-camera
18177
+ * `motion-zones` capability. A cap-settings component (ui-library),
18178
+ * mounted into the device-detail Config tab via the cap-UI
18179
+ * contribution mechanism (the `motion-zones` cap declares a `ui` block).
18180
+ *
18181
+ * Split of concerns (mirrors the analytics zones drawer):
18182
+ * - The editable GRID is painted over the live frame by
18183
+ * `MotionGridCanvas`, registered as a player-overlay layer only
18184
+ * while the operator has toggled "Edit grid" on (OFF by default).
18185
+ * - Every CONTROL lives here, in the settings section, NOT on the
18186
+ * frame: a quick-action bar (All on / All off / Invert), Save /
18187
+ * Revert, and the cell-count status.
18188
+ * - Detection enable + sensitivity are NOT edited here — they are
18189
+ * the camera's on-board motion master switch, owned by the
18190
+ * driver's "On-camera motion" settings section. The grid editor
18191
+ * only paints the mask (`setZone` with a `cells`-only patch;
18192
+ * enable / sensitivity are left untouched camera-side).
18193
+ *
18194
+ * The grid editor draws nothing on cameras without the `motion-zones`
18195
+ * cap — the fetch self-gates on a failed `getOptions`.
18196
+ */
18197
+ /** "Camera doesn't expose the cap" — swallow so the editor stays
18198
+ * hidden on unsupported devices (mirrors `usePTZ`'s gate). */
18199
+ function isAbsentProvider(err) {
18200
+ const msg = err instanceof Error ? err.message : String(err);
18201
+ return msg.includes("provider not available") || msg.includes("motion-zones");
18202
+ }
18203
+ /** Coerce an arbitrary `cells` array to exactly `gridWidth*gridHeight`. */
18204
+ function normaliseCells(cells, options) {
18205
+ const total = options.gridWidth * options.gridHeight;
18206
+ const next = new Array(total);
18207
+ for (let i = 0; i < total; i += 1) next[i] = cells[i] === true;
18208
+ return next;
18209
+ }
18210
+ function cellsEqual(a, b) {
18211
+ if (a.length !== b.length) return false;
18212
+ for (let i = 0; i < a.length; i += 1) if (a[i] !== b[i]) return false;
18213
+ return true;
18214
+ }
18215
+ function MotionZonesSettings({ deviceId }) {
18216
+ const dev = useDeviceProxy(useSystem().trpcClient, deviceId);
18217
+ const [options, setOptions] = (0, react$1.useState)(null);
18218
+ const [unsupported, setUnsupported] = (0, react$1.useState)(false);
18219
+ const [cells, setCells] = (0, react$1.useState)(null);
18220
+ const [committed, setCommitted] = (0, react$1.useState)(null);
18221
+ const [saving, setSaving] = (0, react$1.useState)(false);
18222
+ const [editingGrid, setEditingGrid] = (0, react$1.useState)(false);
18223
+ const seededRef = (0, react$1.useRef)(false);
18224
+ (0, react$1.useEffect)(() => {
18225
+ if (!dev) return void 0;
18226
+ let cancelled = false;
18227
+ (async () => {
18228
+ try {
18229
+ const opts = await dev.motionZones?.getOptions({});
18230
+ if (cancelled || !opts) return;
18231
+ setOptions(opts);
18232
+ if (seededRef.current) return;
18233
+ const status = await dev.motionZones?.getStatus({});
18234
+ if (cancelled || !status) return;
18235
+ seededRef.current = true;
18236
+ const norm = normaliseCells(status.cells, opts);
18237
+ setCommitted(norm);
18238
+ setCells(norm);
18239
+ } catch (err) {
18240
+ if (cancelled) return;
18241
+ if (isAbsentProvider(err)) setUnsupported(true);
18242
+ else console.error("motion-zones load failed", err);
18243
+ }
18244
+ })();
18245
+ return () => {
18246
+ cancelled = true;
18247
+ };
18248
+ }, [dev]);
18249
+ const dirty = (0, react$1.useMemo)(() => cells !== null && committed !== null && !cellsEqual(cells, committed), [cells, committed]);
18250
+ const activeCount = (0, react$1.useMemo)(() => cells ? cells.reduce((n, c) => c ? n + 1 : n, 0) : 0, [cells]);
18251
+ const total = options ? options.gridWidth * options.gridHeight : 0;
18252
+ const setAll = (0, react$1.useCallback)((value) => {
18253
+ if (total > 0) setCells(new Array(total).fill(value));
18254
+ }, [total]);
18255
+ const invert = (0, react$1.useCallback)(() => {
18256
+ setCells((prev) => prev ? prev.map((c) => !c) : prev);
18257
+ }, []);
18258
+ const revert = (0, react$1.useCallback)(() => {
18259
+ if (committed) setCells([...committed]);
18260
+ }, [committed]);
18261
+ const save = (0, react$1.useCallback)(async () => {
18262
+ if (!cells || !dev?.motionZones || !options) return;
18263
+ setSaving(true);
18264
+ try {
18265
+ await dev.motionZones.setZone({ patch: { cells: [...cells] } });
18266
+ const fresh = await dev.motionZones.getStatus({});
18267
+ if (fresh) {
18268
+ const norm = normaliseCells(fresh.cells, options);
18269
+ setCommitted(norm);
18270
+ setCells(norm);
18271
+ }
18272
+ } catch (err) {
18273
+ console.error("motion-zones.setZone failed", err);
18274
+ } finally {
18275
+ setSaving(false);
18276
+ }
18277
+ }, [
18278
+ cells,
18279
+ dev,
18280
+ options
18281
+ ]);
18282
+ usePlayerOverlayLayer((0, react$1.useMemo)(() => editingGrid && !unsupported && options && cells ? {
18283
+ id: "motion-zones",
18284
+ order: 110,
18285
+ node: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MotionGridCanvas, {
18286
+ options,
18287
+ cells,
18288
+ onCellsChange: setCells
18289
+ })
18290
+ } : null, [
18291
+ editingGrid,
18292
+ unsupported,
18293
+ options,
18294
+ cells
18295
+ ]));
18296
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
18297
+ className: "flex flex-col gap-3",
18298
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
18299
+ className: "text-[11px] font-semibold text-foreground-subtle uppercase tracking-wider",
18300
+ children: "Motion Zones"
18301
+ }), unsupported ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
18302
+ className: `${TEXT_HINT} leading-relaxed`,
18303
+ children: "This camera doesn't expose an on-board motion-detection grid."
18304
+ }) : !(!unsupported && options !== null && cells !== null) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
18305
+ className: `${TEXT_HINT} leading-relaxed`,
18306
+ children: "Loading the camera's motion grid…"
18307
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
18308
+ className: `${TEXT_HINT} leading-relaxed`,
18309
+ children: [
18310
+ "Toggle",
18311
+ " ",
18312
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", {
18313
+ className: "text-foreground",
18314
+ children: "Edit grid"
18315
+ }),
18316
+ " to paint the region the camera watches for motion directly on the live frame, then ",
18317
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", {
18318
+ className: "text-foreground",
18319
+ children: "Save"
18320
+ }),
18321
+ " to push the mask to the camera. Detection on/off and sensitivity are set in the",
18322
+ " ",
18323
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", {
18324
+ className: "text-foreground",
18325
+ children: "On-camera motion"
18326
+ }),
18327
+ " section."
18328
+ ]
18329
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
18330
+ className: "flex items-center gap-2 flex-wrap",
18331
+ children: [
18332
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
18333
+ type: "button",
18334
+ onClick: () => setEditingGrid((v) => !v),
18335
+ disabled: saving,
18336
+ "aria-pressed": editingGrid,
18337
+ className: editingGrid ? "rounded-md border border-primary/50 bg-primary/15 px-2.5 py-1 text-[11px] font-medium text-primary hover:bg-primary/25 disabled:opacity-40 transition-colors" : "rounded-md border border-border bg-surface px-2.5 py-1 text-[11px] font-medium text-foreground-subtle hover:bg-surface-hover disabled:opacity-40 transition-colors",
18338
+ children: editingGrid ? "Done editing" : "Edit grid"
18339
+ }),
18340
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
18341
+ type: "button",
18342
+ onClick: () => setAll(true),
18343
+ disabled: saving,
18344
+ className: "rounded-md border border-border bg-surface px-2 py-1 text-[11px] font-medium text-foreground-subtle hover:bg-surface-hover disabled:opacity-40 transition-colors",
18345
+ children: "All on"
18346
+ }),
18347
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
18348
+ type: "button",
18349
+ onClick: () => setAll(false),
18350
+ disabled: saving,
18351
+ className: "rounded-md border border-border bg-surface px-2 py-1 text-[11px] font-medium text-foreground-subtle hover:bg-surface-hover disabled:opacity-40 transition-colors",
18352
+ children: "All off"
18353
+ }),
18354
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
18355
+ type: "button",
18356
+ onClick: invert,
18357
+ disabled: saving,
18358
+ className: "rounded-md border border-border bg-surface px-2 py-1 text-[11px] font-medium text-foreground-subtle hover:bg-surface-hover disabled:opacity-40 transition-colors",
18359
+ children: "Invert"
18360
+ }),
18361
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
18362
+ className: `${TEXT_HINT} ml-1 tabular-nums`,
18363
+ children: [
18364
+ activeCount,
18365
+ " / ",
18366
+ total,
18367
+ " cells · ",
18368
+ options.gridWidth,
18369
+ "×",
18370
+ options.gridHeight
18371
+ ]
18372
+ }),
18373
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "flex-1" }),
18374
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
18375
+ type: "button",
18376
+ onClick: revert,
18377
+ disabled: saving || !dirty,
18378
+ className: "rounded-md border border-border bg-surface px-2 py-1 text-[11px] text-foreground-subtle hover:bg-surface-hover disabled:opacity-40 transition-colors",
18379
+ children: "Revert"
18380
+ }),
18381
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
18382
+ type: "button",
18383
+ onClick: () => void save(),
18384
+ disabled: saving || !dirty,
18385
+ className: "rounded-md border border-primary/50 bg-primary/15 px-2.5 py-1 text-[11px] font-medium text-primary hover:bg-primary/25 disabled:opacity-40 transition-colors",
18386
+ children: saving ? "Saving…" : "Save"
18387
+ })
18388
+ ]
18389
+ })] })]
18390
+ });
18391
+ }
18392
+ //#endregion
18393
+ //#region src/widgets/host-widgets.ts
18394
+ /** Host widgets — ui-library React components embeddable as a
18395
+ * `type:'widget'` ConfigField. `WidgetSlot` resolves these BEFORE the
18396
+ * Module-Federation `WidgetRegistry`. ids are namespaced `host/<name>`. */
18397
+ var HOST_WIDGETS = {
18398
+ "host/motion-zones-grid": MotionZonesSettings,
18399
+ "host/ptz-panel": PtzPanel,
18400
+ "host/ptz-autotrack": AutotrackSection
18401
+ };
18402
+ //#endregion
18403
+ //#region src/composites/widget-slot.tsx
18404
+ /**
18405
+ * <WidgetSlot> — single host-side mount point for any addon-contributed
18406
+ * widget. Consumers reference a widget by its public id
18407
+ * (`<addonId>/<stableId>`), the slot looks the widget's metadata up in
18408
+ * the shared `WidgetRegistry`, resolves the component through the
18409
+ * unified `useRemoteComponent` Module-Federation loader (the SAME path
18410
+ * `ContributionRenderer`'s `kind:'remote'` branch uses), validates host
18411
+ * context against the widget's `requires` metadata, and renders one of:
18412
+ * - skeleton placeholder while the bundle is still loading,
18413
+ * - inline error fallback when the widget id is unknown OR the host
18414
+ * didn't supply a required context (`deviceContext` /
18415
+ * `integrationContext`),
18416
+ * - the resolved component otherwise.
18417
+ *
18418
+ * The slot is intentionally STUPID — no styling beyond the skeleton/
18419
+ * error fallback. Layout (card vs inline, sizing) is the host's
18420
+ * responsibility.
18421
+ */
18422
+ function WidgetSlot(props) {
18423
+ const { widgetId, deviceId } = props;
18424
+ const HostComponent = HOST_WIDGETS[widgetId];
18425
+ if (HostComponent !== void 0) {
18426
+ if (deviceId === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetMissingError, {
18427
+ widgetId,
18428
+ reason: "missing-device-context"
18429
+ });
18430
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HostComponent, { deviceId });
18431
+ }
18432
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RemoteWidgetSlot, { ...props });
18433
+ }
18434
+ /**
18435
+ * Module-Federation remote-widget path — looks the widget's metadata up
18436
+ * in the shared `WidgetRegistry` and resolves it through `useRemoteComponent`.
18437
+ */
18438
+ function RemoteWidgetSlot(props) {
18439
+ const { widgetId } = props;
18440
+ const metadata = useWidgetMetadata(widgetId);
18441
+ if (metadata === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetMissingError, {
18442
+ widgetId,
18443
+ reason: "unknown"
18444
+ });
18445
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResolvedWidgetSlot, {
18446
+ ...props,
18447
+ metadata
18448
+ });
18449
+ }
18450
+ /**
18451
+ * Inner host — resolves the widget component through the unified
18452
+ * `useRemoteComponent` MF loader (shared with `ContributionRenderer`'s
18453
+ * `kind:'remote'` branch). Mounted only once a `metadata` descriptor is
18454
+ * known, so the hook always receives a valid `remote` descriptor.
18455
+ */
18456
+ function ResolvedWidgetSlot(props) {
18457
+ const { widgetId, host = "device-tab", config, deviceId, integrationId, instanceId, size, columns, rows, metadata } = props;
18458
+ const Component = useRemoteComponent(metadata.remote, metadata.bundleUrl);
18459
+ const resolvedInstanceId = (0, react$1.useMemo)(() => instanceId ?? widgetId, [instanceId, widgetId]);
18460
+ if (Component === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetSkeleton, {});
18461
+ if (Component === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetMissingError, {
18462
+ widgetId,
18463
+ reason: "missing-export"
18464
+ });
18465
+ if (metadata.requires.deviceContext && deviceId === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetMissingError, {
18466
+ widgetId,
18467
+ reason: "missing-device-context"
18468
+ });
18469
+ if (metadata.requires.integrationContext && integrationId === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetMissingError, {
18470
+ widgetId,
18471
+ reason: "missing-integration-context"
18472
+ });
18473
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Component, {
18474
+ instanceId: resolvedInstanceId,
18475
+ host,
18476
+ config,
18477
+ deviceId,
18478
+ integrationId,
18479
+ size,
18480
+ columns,
18481
+ rows
18482
+ });
18483
+ }
18484
+ function WidgetSkeleton() {
18485
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
18486
+ className: "rounded-lg border border-border bg-surface/40 p-4 animate-pulse",
18487
+ children: [
18488
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "h-3 w-24 bg-foreground-subtle/20 rounded mb-2" }),
18489
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "h-2 w-full bg-foreground-subtle/10 rounded mb-1" }),
18490
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "h-2 w-3/4 bg-foreground-subtle/10 rounded" })
18491
+ ]
18492
+ });
18493
+ }
18494
+ function WidgetMissingError({ widgetId, reason }) {
18495
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
18496
+ className: "rounded-lg border border-warning/30 bg-warning/10 px-3 py-2 text-xs text-warning",
18497
+ children: reason === "unknown" ? `Widget "${widgetId}" not registered` : reason === "missing-export" ? `Widget "${widgetId}" bundle does not export this stableId` : reason === "missing-device-context" ? `Widget "${widgetId}" requires a deviceId` : `Widget "${widgetId}" requires an integrationId`
18498
+ });
18499
+ }
18500
+ //#endregion
18501
+ //#region src/composites/config-form-field.tsx
18502
+ var INPUT_CLASS = "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";
18503
+ var LABEL_CLASS = "block text-[11px] font-medium text-foreground mb-1";
18504
+ var DESC_CLASS = "text-[10px] text-foreground-subtle mt-0.5";
18505
+ function FieldWrapper({ label, description, required, span, children, translationFn }) {
18506
+ const colSpanClass = span === 2 ? "col-span-2" : span === 3 ? "col-span-3" : span === 4 ? "col-span-4" : "col-span-1";
18507
+ const resolvedLabel = resolveLabel(label, translationFn);
18508
+ const resolvedDescription = resolveLabel(description, translationFn);
18509
+ const hasLabel = resolvedLabel !== void 0 && resolvedLabel !== "";
18510
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
18511
+ className: `${colSpanClass} min-w-0`,
18512
+ children: [
18513
+ hasLabel && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
18514
+ className: LABEL_CLASS,
18515
+ children: [resolvedLabel, required && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
18516
+ className: "text-danger ml-0.5",
18517
+ children: "*"
18518
+ })]
18519
+ }),
18520
+ children,
18521
+ resolvedDescription && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
18522
+ className: DESC_CLASS,
18523
+ children: resolvedDescription
18524
+ })
18525
+ ]
18526
+ });
18527
+ }
18528
+ function TextField({ field, value, onChange, disabled, translationFn }) {
18529
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldWrapper, {
18530
+ label: field.label,
18531
+ description: field.description,
18532
+ required: field.required,
18533
+ span: field.span,
18534
+ translationFn,
18535
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
18536
+ type: field.inputType ?? "text",
18537
+ className: INPUT_CLASS,
18538
+ value: value === void 0 || value === null ? "" : String(value),
18539
+ placeholder: field.placeholder,
18540
+ maxLength: field.maxLength,
18541
+ pattern: field.pattern,
18542
+ disabled: disabled || field.disabled,
18543
+ onChange: (e) => onChange(e.target.value)
18544
+ })
18545
+ });
18546
+ }
18547
+ function NumberField({ field, value, onChange, disabled, translationFn }) {
18548
+ const [local, setLocal] = (0, react$1.useState)(value === void 0 || value === null ? "" : String(value));
18549
+ const focusedRef = (0, react$1.useRef)(false);
18550
+ (0, react$1.useEffect)(() => {
18551
+ if (focusedRef.current) return;
18552
+ setLocal(value === void 0 || value === null ? "" : String(value));
18553
+ }, [value]);
18554
+ const handleChange = (raw) => {
18555
+ setLocal(raw);
18556
+ if (raw === "" || raw === "-") {
18557
+ onChange(void 0);
18558
+ return;
18559
+ }
18560
+ const parsed = Number(raw);
18561
+ if (Number.isNaN(parsed)) return;
18562
+ onChange(parsed);
18563
+ };
18564
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldWrapper, {
18565
+ label: field.label,
18566
+ description: field.description,
18567
+ required: field.required,
18568
+ span: field.span,
18569
+ translationFn,
18570
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
18571
+ className: "flex items-center gap-1",
18572
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
18573
+ type: "number",
18574
+ className: INPUT_CLASS,
18575
+ value: local,
18576
+ placeholder: field.placeholder,
18577
+ min: field.min,
18578
+ max: field.max,
18579
+ step: field.step,
18580
+ disabled: disabled || field.disabled,
18581
+ onFocus: () => {
18582
+ focusedRef.current = true;
18583
+ },
18584
+ onBlur: () => {
18585
+ focusedRef.current = false;
18586
+ },
18587
+ onChange: (e) => handleChange(e.target.value)
18588
+ }), field.unit && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
18589
+ className: "text-xs text-foreground-subtle whitespace-nowrap",
17181
18590
  children: field.unit
17182
18591
  })]
17183
18592
  })
@@ -18286,18 +19695,12 @@ function AddonActionButtonField({ field, values, disabled, onAction }) {
18286
19695
  }
18287
19696
  function WidgetField({ field }) {
18288
19697
  const deviceId = useDeviceId();
18289
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldWrapper, {
18290
- label: field.label,
18291
- description: field.description,
18292
- required: field.required,
18293
- span: field.span,
18294
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetSlot, {
18295
- widgetId: field.widgetId,
18296
- host: "device-tab",
18297
- config: field.widgetConfig,
18298
- deviceId: deviceId ?? void 0,
18299
- instanceId: field.key
18300
- })
19698
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetSlot, {
19699
+ widgetId: field.widgetId,
19700
+ host: "device-tab",
19701
+ config: field.widgetConfig,
19702
+ deviceId: deviceId ?? void 0,
19703
+ instanceId: field.key
18301
19704
  });
18302
19705
  }
18303
19706
  function formatReadonlyValue(value, unit) {
@@ -18788,7 +20191,7 @@ function ConfigFormBuilder({ schema, values, onChange, disabled, translationFn,
18788
20191
  * + patch loop. `onAfterChange` fires once the patch is applied so
18789
20192
  * consumers can re-read any derived state.
18790
20193
  */
18791
- var Chevron = ({ open }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
20194
+ var Chevron$1 = ({ open }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
18792
20195
  className: `h-3 w-3 transition-transform ${open ? "rotate-90" : ""}`,
18793
20196
  viewBox: "0 0 24 24",
18794
20197
  fill: "none",
@@ -18953,7 +20356,7 @@ function AddonGlobalSettingsForm({ trpc, addonId, nodeId, title, disabled, onAft
18953
20356
  className: "w-full px-4 py-2 flex items-center gap-2 hover:bg-surface-hover text-left",
18954
20357
  "aria-expanded": open,
18955
20358
  children: [
18956
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Chevron, { open }),
20359
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Chevron$1, { open }),
18957
20360
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
18958
20361
  className: "text-xs font-semibold text-foreground uppercase tracking-wide flex-1",
18959
20362
  children: title ?? addonId
@@ -19524,183 +20927,37 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
19524
20927
  className: "h-3.5 w-3.5",
19525
20928
  viewBox: "0 0 24 24",
19526
20929
  fill: "none",
19527
- stroke: "currentColor",
19528
- strokeWidth: "2",
19529
- strokeLinecap: "round",
19530
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M8 3H5a2 2 0 00-2 2v3m18 0V5a2 2 0 00-2-2h-3m0 18h3a2 2 0 002-2v-3M3 16v3a2 2 0 002 2h3" })
19531
- })
19532
- })]
19533
- })]
19534
- })
19535
- ]
19536
- });
19537
- }
19538
- function ToolbarButton$1({ onClick, title, children }) {
19539
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
19540
- onClick,
19541
- title,
19542
- className: "rounded-full p-1.5 text-white/80 hover:text-white hover:bg-white/20 transition-colors",
19543
- children
19544
- });
19545
- }
19546
- async function waitForIceGathering(pc) {
19547
- if (pc.iceGatheringState === "complete") return;
19548
- return new Promise((resolve) => {
19549
- const handler = () => {
19550
- if (pc.iceGatheringState === "complete") {
19551
- pc.removeEventListener("icegatheringstatechange", handler);
19552
- resolve();
19553
- }
19554
- };
19555
- pc.addEventListener("icegatheringstatechange", handler);
19556
- setTimeout(resolve, 5e3);
19557
- });
19558
- }
19559
- //#endregion
19560
- //#region src/contexts/player-overlays.tsx
19561
- /**
19562
- * Player overlay registry — pluggable layers + toolbar buttons for
19563
- * the device-detail live-frame `StreamPanel`.
19564
- *
19565
- * Why a registry: previously `StreamPanelContent` hardcoded each
19566
- * overlay (PTZ, intercom toggle, zone editor) and threaded the
19567
- * editing state by hand. Adding a new feature (audio waveform,
19568
- * detection bbox tracker, recording controls, …) meant another
19569
- * round of plumbing through the host file and StreamPanel's prop
19570
- * surface. The registry lets a sibling component (Detection tab,
19571
- * Live Stats tab, an addon-page extension) declare its own layer
19572
- * imperatively via a hook, and the host renders whatever's
19573
- * registered.
19574
- *
19575
- * Two registries live side-by-side:
19576
- *
19577
- * 1. **layers** — absolute-positioned React nodes drawn over the
19578
- * video frame (zones polygon canvas, motion bbox overlay,
19579
- * audio waveform). Rendered ordered by `order` (lower first
19580
- * → bottom; higher → top); the last-registered layer wins
19581
- * ties.
19582
- *
19583
- * 2. **toolbar buttons** — controls surfaced in the player's
19584
- * always-visible toolbar cluster (next to Intercom + Play/Stop).
19585
- * Each carries an icon, label, controlled `active` flag, and
19586
- * a click handler. The host (StreamPanel) renders them
19587
- * uniformly; tone variants stay simple (`'default' | 'primary'`).
19588
- *
19589
- * Lifecycle: hooks register their layer / button on mount and
19590
- * unregister on unmount, so swapping tabs (e.g. leaving Detection)
19591
- * automatically tears down the registration. Re-registering with
19592
- * the same `id` replaces the prior entry — meaning a single hook
19593
- * call can update its overlay/button props on every render without
19594
- * leaking entries.
19595
- *
19596
- * Provider scope: typically wraps the whole device-detail subtree
19597
- * so the StreamPanel + every tab share the same registry. One
19598
- * provider per `deviceId`; switching device IDs unmounts the
19599
- * provider naturally.
19600
- */
19601
- var PlayerOverlaysStateContext = createSharedContext("camstack:player-overlays-state", null);
19602
- var PlayerOverlaysActionsContext = createSharedContext("camstack:player-overlays-actions", null);
19603
- function PlayerOverlaysProvider({ children }) {
19604
- const [layers, setLayers] = (0, react$1.useState)(() => /* @__PURE__ */ new Map());
19605
- const [buttons, setButtons] = (0, react$1.useState)(() => /* @__PURE__ */ new Map());
19606
- const setLayer = (0, react$1.useCallback)((layer) => {
19607
- setLayers((prev) => {
19608
- const next = new Map(prev);
19609
- next.set(layer.id, layer);
19610
- return next;
19611
- });
19612
- }, []);
19613
- const removeLayer = (0, react$1.useCallback)((id) => {
19614
- setLayers((prev) => {
19615
- if (!prev.has(id)) return prev;
19616
- const next = new Map(prev);
19617
- next.delete(id);
19618
- return next;
19619
- });
19620
- }, []);
19621
- const setButton = (0, react$1.useCallback)((button) => {
19622
- setButtons((prev) => {
19623
- const next = new Map(prev);
19624
- next.set(button.id, button);
19625
- return next;
19626
- });
19627
- }, []);
19628
- const removeButton = (0, react$1.useCallback)((id) => {
19629
- setButtons((prev) => {
19630
- if (!prev.has(id)) return prev;
19631
- const next = new Map(prev);
19632
- next.delete(id);
19633
- return next;
19634
- });
19635
- }, []);
19636
- const stateValue = (0, react$1.useMemo)(() => ({
19637
- layers,
19638
- buttons
19639
- }), [layers, buttons]);
19640
- const actionsValue = (0, react$1.useMemo)(() => ({
19641
- setLayer,
19642
- removeLayer,
19643
- setButton,
19644
- removeButton
19645
- }), [
19646
- setLayer,
19647
- removeLayer,
19648
- setButton,
19649
- removeButton
19650
- ]);
19651
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlayerOverlaysStateContext.Provider, {
19652
- value: stateValue,
19653
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlayerOverlaysActionsContext.Provider, {
19654
- value: actionsValue,
19655
- children
19656
- })
20930
+ stroke: "currentColor",
20931
+ strokeWidth: "2",
20932
+ strokeLinecap: "round",
20933
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M8 3H5a2 2 0 00-2 2v3m18 0V5a2 2 0 00-2-2h-3m0 18h3a2 2 0 002-2v-3M3 16v3a2 2 0 002 2h3" })
20934
+ })
20935
+ })]
20936
+ })]
20937
+ })
20938
+ ]
19657
20939
  });
19658
20940
  }
19659
- /** Snapshot of registered layers ordered by `order` (asc, ties resolved
19660
- * by insertion order of the underlying Map). Returns `[]` outside a
19661
- * provider. */
19662
- function usePlayerOverlayLayers() {
19663
- const state = (0, react$1.useContext)(PlayerOverlaysStateContext);
19664
- return (0, react$1.useMemo)(() => {
19665
- if (!state) return [];
19666
- return [...state.layers.values()].sort((a, b) => a.order - b.order);
19667
- }, [state]);
19668
- }
19669
- /** Snapshot of registered toolbar buttons, ordered by `order` (asc). */
19670
- function usePlayerToolbarButtons() {
19671
- const state = (0, react$1.useContext)(PlayerOverlaysStateContext);
19672
- return (0, react$1.useMemo)(() => {
19673
- if (!state) return [];
19674
- return [...state.buttons.values()].sort((a, b) => a.order - b.order);
19675
- }, [state]);
19676
- }
19677
- /**
19678
- * Register an overlay layer for the lifetime of the calling component.
19679
- * Re-registers on every render with the latest spec; auto-unregisters
19680
- * on unmount. Pass `null` to skip registration when the layer is
19681
- * conditionally enabled (the hook still runs every render — keeps
19682
- * react-hook order stable across spec === null toggles).
19683
- *
19684
- * Callers that build the spec inline should memoise it (`useMemo`)
19685
- * to avoid re-registering on every parent render — context-write
19686
- * effects depend on referential equality of the spec object.
19687
- */
19688
- function usePlayerOverlayLayer(spec) {
19689
- const actions = (0, react$1.useContext)(PlayerOverlaysActionsContext);
19690
- (0, react$1.useEffect)(() => {
19691
- if (!actions || !spec) return void 0;
19692
- actions.setLayer(spec);
19693
- return () => actions.removeLayer(spec.id);
19694
- }, [actions, spec]);
20941
+ function ToolbarButton$1({ onClick, title, children }) {
20942
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
20943
+ onClick,
20944
+ title,
20945
+ className: "rounded-full p-1.5 text-white/80 hover:text-white hover:bg-white/20 transition-colors",
20946
+ children
20947
+ });
19695
20948
  }
19696
- /** Same shape as `usePlayerOverlayLayer`, scoped to toolbar buttons. */
19697
- function usePlayerToolbarButton(spec) {
19698
- const actions = (0, react$1.useContext)(PlayerOverlaysActionsContext);
19699
- (0, react$1.useEffect)(() => {
19700
- if (!actions || !spec) return void 0;
19701
- actions.setButton(spec);
19702
- return () => actions.removeButton(spec.id);
19703
- }, [actions, spec]);
20949
+ async function waitForIceGathering(pc) {
20950
+ if (pc.iceGatheringState === "complete") return;
20951
+ return new Promise((resolve) => {
20952
+ const handler = () => {
20953
+ if (pc.iceGatheringState === "complete") {
20954
+ pc.removeEventListener("icegatheringstatechange", handler);
20955
+ resolve();
20956
+ }
20957
+ };
20958
+ pc.addEventListener("icegatheringstatechange", handler);
20959
+ setTimeout(resolve, 5e3);
20960
+ });
19704
20961
  }
19705
20962
  //#endregion
19706
20963
  //#region src/composites/stream-panel.tsx
@@ -21508,12 +22765,11 @@ function CopyButton({ value, label, className, disabled }) {
21508
22765
  * Alexa / HAP / MQTT specifics here. Mount it next to an export addon's
21509
22766
  * standard settings form (e.g. in the addon-settings modal).
21510
22767
  *
21511
- * Routing note: `device-export` is a collection cap; the codegen'd
21512
- * router resolves `{ nodeId }` to the first registered provider. Export
21513
- * addons are all `hub-only`, so the panel queries `nodeId: 'hub'`. When
21514
- * multiple device-export addons are co-installed the cap-router today
21515
- * resolves to the first provider only (per-provider routing is a
21516
- * separate follow-up) — identical behaviour to the pages it replaces.
22768
+ * Routing note: `device-export` is a collection cap. Export addons are
22769
+ * all `hub-only`, so the panel queries `nodeId: 'hub'`. It also passes
22770
+ * `addonId` so the codegen'd cap-router resolves THIS addon's provider
22771
+ * within the local collection — when multiple device-export addons are
22772
+ * co-installed each panel shows its own provider, not provider[0].
21517
22773
  */
21518
22774
  var ExposedDeviceArraySchema = zod.z.array(_camstack_types.ExposedDeviceSchema);
21519
22775
  var STATUS_POLL_INTERVAL_MS = 1e4;
@@ -21604,11 +22860,17 @@ function SetupSection({ setup }) {
21604
22860
  */
21605
22861
  function DeviceExportPanel({ addonId, onOpenDevice }) {
21606
22862
  const queryClient = (0, _tanstack_react_query.useQueryClient)();
21607
- const statusQuery = useDeviceExportGetStatus({ nodeId: "hub" }, {
22863
+ const statusQuery = useDeviceExportGetStatus({
22864
+ nodeId: "hub",
22865
+ addonId
22866
+ }, {
21608
22867
  refetchInterval: STATUS_POLL_INTERVAL_MS,
21609
22868
  retry: false
21610
22869
  });
21611
- const exposedQuery = useDeviceExportListExposedDevices({ nodeId: "hub" }, {
22870
+ const exposedQuery = useDeviceExportListExposedDevices({
22871
+ nodeId: "hub",
22872
+ addonId
22873
+ }, {
21612
22874
  refetchInterval: STATUS_POLL_INTERVAL_MS,
21613
22875
  retry: false
21614
22876
  });
@@ -21680,13 +22942,18 @@ function DeviceExportPanel({ addonId, onOpenDevice }) {
21680
22942
  disabled: unexposeMutation.isPending,
21681
22943
  onClick: () => unexposeMutation.mutate({
21682
22944
  deviceId: row.deviceId,
21683
- nodeId: "hub"
22945
+ nodeId: "hub",
22946
+ addonId
21684
22947
  }),
21685
22948
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Unlink2, { className: "h-3.5 w-3.5 mr-1" }), "Unexpose"]
21686
22949
  })]
21687
22950
  })
21688
22951
  }
21689
- ], [onOpenDevice, unexposeMutation]);
22952
+ ], [
22953
+ onOpenDevice,
22954
+ unexposeMutation,
22955
+ addonId
22956
+ ]);
21690
22957
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
21691
22958
  className: "rounded-lg border border-border bg-surface overflow-hidden",
21692
22959
  children: [
@@ -22176,7 +23443,7 @@ var LEVEL_OPTIONS = [
22176
23443
  "error"
22177
23444
  ];
22178
23445
  /** Max live entries kept in the ring buffer. */
22179
- var MAX_LIVE_ENTRIES$2 = 500;
23446
+ var MAX_LIVE_ENTRIES$1 = 500;
22180
23447
  function passesLevel(logLevel, filterLevel) {
22181
23448
  if (!filterLevel) return true;
22182
23449
  return (LEVEL_SEVERITY[logLevel] ?? 0) >= (LEVEL_SEVERITY[filterLevel] ?? 0);
@@ -22205,7 +23472,7 @@ function LogStream({ agentId: propsAgentId, addonId: propsAddonId, deviceId: pro
22205
23472
  integrationId,
22206
23473
  requestId
22207
23474
  ]);
22208
- const fallbackBuffer = useLiveBuffer(MAX_LIVE_ENTRIES$2);
23475
+ const fallbackBuffer = useLiveBuffer(MAX_LIVE_ENTRIES$1);
22209
23476
  const buffer = externalBuffer ?? fallbackBuffer;
22210
23477
  const liveLogs = buffer.entries;
22211
23478
  const [clearedAt, setClearedAt] = (0, react$1.useState)(0);
@@ -22532,7 +23799,7 @@ function LogStream({ agentId: propsAgentId, addonId: propsAddonId, deviceId: pro
22532
23799
  }),
22533
23800
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
22534
23801
  className: "px-1 py-1 align-top w-5",
22535
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RowCopyButton$2, {
23802
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RowCopyButton$1, {
22536
23803
  copied: copiedRowKey === key,
22537
23804
  onCopy: () => copyOne(log, key)
22538
23805
  })
@@ -22571,7 +23838,7 @@ function ScopeBadge$2({ label, value }) {
22571
23838
  * "did it work" affordance — same UX language the toolbar Copy
22572
23839
  * button already uses for its `Copied!` label.
22573
23840
  */
22574
- function RowCopyButton$2({ copied, onCopy }) {
23841
+ function RowCopyButton$1({ copied, onCopy }) {
22575
23842
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
22576
23843
  type: "button",
22577
23844
  title: "Copy this row",
@@ -22896,7 +24163,7 @@ function hasContent(entry) {
22896
24163
  if (cat === _camstack_types.EventCategory.DetectionResult || cat === _camstack_types.EventCategory.PipelineAudioInferenceResult) return summarizeEvent(cat, entry.data) !== null;
22897
24164
  return true;
22898
24165
  }
22899
- var MAX_LIVE_ENTRIES$1 = 300;
24166
+ var MAX_LIVE_ENTRIES = 300;
22900
24167
  function EventStream({ agentId, addonId, deviceId, defaultCategories, categories: legacyCategories, category: propsCategory, maxHeight = "max-h-96", limit = 50, showCategoryFilter = true, showFilters: _showFilters = false, liveBuffer: externalBuffer, onClose, className }) {
22901
24168
  const defaultsArray = (0, react$1.useMemo)(() => defaultCategories ?? legacyCategories ?? DEFAULT_EVENT_CATEGORIES, [defaultCategories, legacyCategories]);
22902
24169
  const defaultsKey = (0, react$1.useMemo)(() => [...defaultsArray].sort().join(","), [defaultsArray]);
@@ -22908,7 +24175,7 @@ function EventStream({ agentId, addonId, deviceId, defaultCategories, categories
22908
24175
  setActiveCategories(new Set(defaultsArray));
22909
24176
  }
22910
24177
  }, [defaultsKey, defaultsArray]);
22911
- const fallbackBuffer = useLiveBuffer(MAX_LIVE_ENTRIES$1);
24178
+ const fallbackBuffer = useLiveBuffer(MAX_LIVE_ENTRIES);
22912
24179
  const buffer = externalBuffer ?? fallbackBuffer;
22913
24180
  const liveEvents = buffer.entries;
22914
24181
  const [autoScroll, setAutoScroll] = (0, react$1.useState)(true);
@@ -23232,7 +24499,7 @@ function EventStream({ agentId, addonId, deviceId, defaultCategories, categories
23232
24499
  className: "text-foreground-subtle whitespace-nowrap w-[70px] shrink-0 pt-0.5",
23233
24500
  children: new Date(event.timestamp).toLocaleTimeString()
23234
24501
  }),
23235
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RowCopyButton$1, {
24502
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RowCopyButton, {
23236
24503
  copied: copiedRowId === event.id,
23237
24504
  onCopy: () => copyOne(event)
23238
24505
  }),
@@ -23422,7 +24689,7 @@ function ScopeBadge$1({ label, value }) {
23422
24689
  * click doesn't toggle the row's expand state. Flashes a Check
23423
24690
  * for ~1s after copy.
23424
24691
  */
23425
- function RowCopyButton$1({ copied, onCopy }) {
24692
+ function RowCopyButton({ copied, onCopy }) {
23426
24693
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
23427
24694
  type: "button",
23428
24695
  title: "Copy this event",
@@ -23689,162 +24956,137 @@ function StatusBadge$1({ status }) {
23689
24956
  //#endregion
23690
24957
  //#region src/composites/state-values-stream.tsx
23691
24958
  /**
23692
- * StateValuesStream — runtime-state change viewer for ui-library.
24959
+ * StateValuesStream — live current-state tree for ui-library.
24960
+ *
24961
+ * Renders a single device's CURRENT runtime-state as a scrollable,
24962
+ * collapsible hierarchical tree (NOT a chronological change log).
23693
24963
  *
23694
- * Subscribes to `EventCategory.DeviceStateChanged` events scoped to a
23695
- * single device. Each row shows: timestamp, capName, slice JSON.
23696
- * Supports cap-name multiselect filter + free-text search.
24964
+ * - Seed: on mount, `trpc.deviceState.getAllSnapshots` returns the
24965
+ * whole-system snapshot; we pick out this device's entry and build
24966
+ * a `Map<capName, slice>` of its current state.
24967
+ * - Realtime: an `EventCategory.DeviceStateChanged` tRPC subscription
24968
+ * scoped to the device patches each cap's slice into the map in
24969
+ * place — the matching tree node re-renders, no rows are appended.
24970
+ * - Tree: top level = cap names (sorted, expanded by default); each
24971
+ * cap node expands to its slice keys; nested objects/arrays expand
24972
+ * recursively, deep nesting collapsed by default. A per-cap
24973
+ * "updated Ns ago" indicator surfaces realtime activity.
24974
+ * - Filter/search: cap-name multiselect popover + free-text search,
24975
+ * both applied to the tree (filter which caps show; search matches
24976
+ * keys/values anywhere in the slice).
23697
24977
  *
23698
- * Live subscription is mounted only while this component is rendered,
24978
+ * The live subscription is mounted only while this component renders,
23699
24979
  * so the parent can mount/unmount it on tab switches and the tRPC
23700
24980
  * subscription is torn down automatically when hidden.
23701
24981
  */
23702
- function isStateChangeForDevice(raw, deviceId) {
23703
- if (!raw || typeof raw !== "object") return false;
23704
- const e = raw;
23705
- if (e.category !== _camstack_types.EventCategory.DeviceStateChanged) return false;
23706
- const data = e.data;
23707
- if (!data || typeof data !== "object") return false;
23708
- if (data.deviceId !== deviceId) return false;
23709
- if (typeof data.capName !== "string") return false;
23710
- if (!data.slice || typeof data.slice !== "object") return false;
23711
- return true;
23712
- }
23713
- function toEntry(raw) {
24982
+ /** Narrow an untrusted live-stream event to a `DeviceStateChanged`
24983
+ * for THIS device, returning the cap name + slice it carries. */
24984
+ function readStateChange(raw, deviceId) {
23714
24985
  if (!raw || typeof raw !== "object") return null;
23715
24986
  const e = raw;
23716
- const id = typeof e.id === "string" ? e.id : null;
23717
- const timestamp = typeof e.timestamp === "string" ? e.timestamp : null;
24987
+ if (e.category !== _camstack_types.EventCategory.DeviceStateChanged) return null;
23718
24988
  const data = e.data;
23719
- if (!id || !timestamp || !data || typeof data !== "object") return null;
23720
- if (typeof data.capName !== "string" || !data.slice || typeof data.slice !== "object") return null;
24989
+ if (!data || typeof data !== "object") return null;
24990
+ if (data.deviceId !== deviceId) return null;
24991
+ if (typeof data.capName !== "string") return null;
24992
+ if (!data.slice || typeof data.slice !== "object") return null;
23721
24993
  return {
23722
- id,
23723
- timestamp,
23724
24994
  capName: data.capName,
23725
24995
  slice: data.slice
23726
24996
  };
23727
24997
  }
23728
- var MAX_LIVE_ENTRIES = 300;
24998
+ function isSystemSnapshot(raw) {
24999
+ return !!raw && typeof raw === "object" && !Array.isArray(raw);
25000
+ }
23729
25001
  /**
23730
25002
  * Every device-scoped cap name, sorted alphabetically — same shape
23731
25003
  * as `ALL_EVENT_CATEGORIES` in event-stream.tsx so the filter
23732
25004
  * surface is symmetric across the three live-streams (Logs, Events,
23733
- * State). Computed once at module load; the user can pick from this
23734
- * full list rather than only the caps that have already emitted at
23735
- * least one slice change in the current session.
25005
+ * State). Computed once at module load.
23736
25006
  */
23737
25007
  var ALL_DEVICE_CAP_NAMES = Object.freeze([...new Set(_camstack_types.ALL_CAPABILITY_DEFINITIONS.filter((c) => c.scope === "device").map((c) => c.name))].sort());
23738
- function StateValuesStream({ deviceId, defaultCaps, maxHeight = "max-h-96", limit = 50, liveBuffer: externalBuffer, onClose, className }) {
25008
+ /** Depth at which tree nodes start collapsed. Cap nodes (depth 0) and
25009
+ * their direct slice keys (depth 1) render expanded; anything nested
25010
+ * deeper opens on demand. */
25011
+ var DEFAULT_EXPAND_DEPTH = 1;
25012
+ function StateValuesStream({ deviceId, defaultCaps, maxHeight = "max-h-96", onClose, className }) {
23739
25013
  const [activeCaps, setActiveCaps] = (0, react$1.useState)((0, react$1.useMemo)(() => new Set(defaultCaps ?? []), [defaultCaps]));
23740
25014
  const [searchText, setSearchText] = (0, react$1.useState)("");
23741
- const [autoScroll, setAutoScroll] = (0, react$1.useState)(true);
23742
- const [expandedRows, setExpandedRows] = (0, react$1.useState)(/* @__PURE__ */ new Set());
23743
- const [clearedAt, setClearedAt] = (0, react$1.useState)(0);
23744
25015
  const [filterOpen, setFilterOpen] = (0, react$1.useState)(false);
23745
25016
  const [filterSearch, setFilterSearch] = (0, react$1.useState)("");
23746
25017
  const filterRootRef = (0, react$1.useRef)(null);
23747
- const fallbackBuffer = useLiveBuffer(MAX_LIVE_ENTRIES);
23748
- const buffer = externalBuffer ?? fallbackBuffer;
23749
- const liveEvents = buffer.entries;
23750
- const scrollRef = (0, react$1.useRef)(null);
25018
+ const [capStates, setCapStates] = (0, react$1.useState)(/* @__PURE__ */ new Map());
25019
+ const [collapseOverrides, setCollapseOverrides] = (0, react$1.useState)(/* @__PURE__ */ new Map());
25020
+ const [, setNowTick] = (0, react$1.useState)(0);
23751
25021
  const prevDeviceRef = (0, react$1.useRef)(deviceId);
23752
25022
  (0, react$1.useEffect)(() => {
23753
25023
  if (prevDeviceRef.current !== deviceId) {
23754
25024
  prevDeviceRef.current = deviceId;
23755
- buffer.reset();
23756
- setExpandedRows(/* @__PURE__ */ new Set());
23757
- setClearedAt(0);
25025
+ setCapStates(/* @__PURE__ */ new Map());
25026
+ setCollapseOverrides(/* @__PURE__ */ new Map());
23758
25027
  }
23759
- }, [deviceId, buffer]);
23760
- const recentInput = (0, react$1.useMemo)(() => ({
23761
- deviceId,
23762
- category: _camstack_types.EventCategory.DeviceStateChanged,
23763
- limit
23764
- }), [deviceId, limit]);
23765
- const { data: initialEvents, isLoading } = trpc.systemEvents.getRecent.useQuery(recentInput, { staleTime: 3e4 });
23766
- const activeCapsRef = (0, react$1.useRef)(activeCaps);
25028
+ }, [deviceId]);
25029
+ const { data: snapshot, isLoading } = trpc.deviceState.getAllSnapshots.useQuery({}, { staleTime: 3e4 });
25030
+ const seededRef = (0, react$1.useRef)(false);
25031
+ (0, react$1.useEffect)(() => {
25032
+ if (seededRef.current) return;
25033
+ if (!isSystemSnapshot(snapshot)) return;
25034
+ const deviceSlices = snapshot[String(deviceId)];
25035
+ seededRef.current = true;
25036
+ if (!deviceSlices) return;
25037
+ const now = Date.now();
25038
+ setCapStates((prev) => {
25039
+ const next = new Map(prev);
25040
+ for (const [capName, slice] of Object.entries(deviceSlices)) {
25041
+ if (next.has(capName)) continue;
25042
+ next.set(capName, {
25043
+ slice,
25044
+ updatedAt: now
25045
+ });
25046
+ }
25047
+ return next;
25048
+ });
25049
+ }, [snapshot, deviceId]);
23767
25050
  (0, react$1.useEffect)(() => {
23768
- activeCapsRef.current = activeCaps;
23769
- }, [activeCaps]);
25051
+ seededRef.current = false;
25052
+ }, [deviceId]);
23770
25053
  trpc.systemEvents.subscribe.useSubscription({
23771
25054
  deviceId,
23772
25055
  category: _camstack_types.EventCategory.DeviceStateChanged
23773
25056
  }, { onData: (raw) => {
23774
- if (!isStateChangeForDevice(raw, deviceId)) return;
23775
- const entry = toEntry(raw);
23776
- if (!entry) return;
23777
- const active = activeCapsRef.current;
23778
- if (active.size > 0 && !active.has(entry.capName)) return;
23779
- buffer.append(entry);
25057
+ const change = readStateChange(raw, deviceId);
25058
+ if (!change) return;
25059
+ setCapStates((prev) => {
25060
+ const next = new Map(prev);
25061
+ next.set(change.capName, {
25062
+ slice: change.slice,
25063
+ updatedAt: Date.now()
25064
+ });
25065
+ return next;
25066
+ });
23780
25067
  } });
23781
- const prevLiveCountRef = (0, react$1.useRef)(liveEvents.length);
23782
25068
  (0, react$1.useEffect)(() => {
23783
- const el = scrollRef.current;
23784
- if (!el) {
23785
- prevLiveCountRef.current = liveEvents.length;
23786
- return;
23787
- }
23788
- const prevCount = prevLiveCountRef.current;
23789
- prevLiveCountRef.current = liveEvents.length;
23790
- if (liveEvents.length <= prevCount) return;
23791
- if (autoScroll && el.scrollTop <= 8) el.scrollTo({ top: 0 });
23792
- else if (!autoScroll && el.scrollTop > 0) {
23793
- const firstRow = el.firstElementChild?.firstElementChild;
23794
- const rowHeight = firstRow instanceof HTMLElement ? firstRow.offsetHeight : 28;
23795
- const newCount = liveEvents.length - prevCount;
23796
- el.scrollTop += rowHeight * newCount;
23797
- }
23798
- }, [liveEvents.length, autoScroll]);
23799
- const allEntries = (0, react$1.useMemo)(() => {
23800
- const byId = /* @__PURE__ */ new Map();
23801
- for (const e of initialEvents ?? []) {
23802
- const entry = toEntry(e);
23803
- if (entry) byId.set(entry.id, entry);
23804
- }
23805
- for (const e of liveEvents) byId.set(e.id, e);
23806
- let list = [...byId.values()];
23807
- if (clearedAt > 0) list = list.filter((e) => new Date(e.timestamp).getTime() > clearedAt);
23808
- if (activeCaps.size > 0) list = list.filter((e) => activeCaps.has(e.capName));
25069
+ const id = setInterval(() => setNowTick((t) => t + 1), 5e3);
25070
+ return () => clearInterval(id);
25071
+ }, []);
25072
+ const visibleCaps = (0, react$1.useMemo)(() => {
25073
+ const caps = new Set(ALL_DEVICE_CAP_NAMES);
25074
+ for (const capName of capStates.keys()) caps.add(capName);
25075
+ return [...caps].sort();
25076
+ }, [capStates]);
25077
+ const filteredCapNames = (0, react$1.useMemo)(() => {
23809
25078
  const needle = searchText.trim().toLowerCase();
23810
- if (needle) list = list.filter((e) => {
23811
- if (e.capName.toLowerCase().includes(needle)) return true;
23812
- try {
23813
- if (new Date(e.timestamp).toLocaleTimeString().toLowerCase().includes(needle)) return true;
23814
- } catch {}
23815
- try {
23816
- if (Object.entries(e.slice).map(([k, v]) => `${k}=${formatValue(v)}`).join(" · ").toLowerCase().includes(needle)) return true;
23817
- } catch {}
23818
- try {
23819
- if (JSON.stringify(e.slice).toLowerCase().includes(needle)) return true;
23820
- } catch {}
23821
- return false;
25079
+ return [...capStates.keys()].sort().filter((capName) => {
25080
+ if (activeCaps.size > 0 && !activeCaps.has(capName)) return false;
25081
+ if (!needle) return true;
25082
+ if (capName.toLowerCase().includes(needle)) return true;
25083
+ return sliceMatchesSearch(capStates.get(capName)?.slice ?? {}, needle);
23822
25084
  });
23823
- return list.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
23824
25085
  }, [
23825
- initialEvents,
23826
- liveEvents,
25086
+ capStates,
23827
25087
  activeCaps,
23828
- searchText,
23829
- clearedAt
25088
+ searchText
23830
25089
  ]);
23831
- const visibleCaps = (0, react$1.useMemo)(() => {
23832
- const caps = new Set(ALL_DEVICE_CAP_NAMES);
23833
- for (const e of initialEvents ?? []) {
23834
- const entry = toEntry(e);
23835
- if (entry) caps.add(entry.capName);
23836
- }
23837
- for (const e of liveEvents) caps.add(e.capName);
23838
- return [...caps].sort();
23839
- }, [initialEvents, liveEvents]);
23840
- const toggleRow = (0, react$1.useCallback)((id) => {
23841
- setExpandedRows((prev) => {
23842
- const next = new Set(prev);
23843
- if (next.has(id)) next.delete(id);
23844
- else next.add(id);
23845
- return next;
23846
- });
23847
- }, []);
23848
25090
  const toggleCap = (0, react$1.useCallback)((cap) => {
23849
25091
  setActiveCaps((prev) => {
23850
25092
  const next = new Set(prev);
@@ -23857,34 +25099,36 @@ function StateValuesStream({ deviceId, defaultCaps, maxHeight = "max-h-96", limi
23857
25099
  setActiveCaps(new Set(defaultCaps ?? []));
23858
25100
  setSearchText("");
23859
25101
  }, [defaultCaps]);
23860
- const handleClear = (0, react$1.useCallback)(() => {
23861
- setClearedAt(Date.now());
23862
- buffer.reset();
23863
- }, [buffer]);
23864
- const [copiedAll, setCopiedAll] = (0, react$1.useState)(false);
23865
- const [copiedRowId, setCopiedRowId] = (0, react$1.useState)(null);
23866
- const formatEntryForCopy = (0, react$1.useCallback)((entry) => {
23867
- return `${new Date(entry.timestamp).toISOString()} [${entry.capName}] slice=${JSON.stringify(entry.slice)}`;
23868
- }, []);
23869
- const handleCopyAll = (0, react$1.useCallback)(() => {
23870
- const lines = allEntries.map(formatEntryForCopy);
23871
- navigator.clipboard.writeText(lines.join("\n")).then(() => {
23872
- setCopiedAll(true);
23873
- setTimeout(() => setCopiedAll(false), 1500);
23874
- });
23875
- }, [allEntries, formatEntryForCopy]);
23876
- const copyOne = (0, react$1.useCallback)((entry) => {
23877
- navigator.clipboard.writeText(formatEntryForCopy(entry)).then(() => {
23878
- setCopiedRowId(entry.id);
23879
- setTimeout(() => setCopiedRowId((prev) => prev === entry.id ? null : prev), 1200);
23880
- });
23881
- }, [formatEntryForCopy]);
23882
25102
  const handleSelectAllCaps = (0, react$1.useCallback)(() => {
23883
25103
  setActiveCaps(new Set(visibleCaps));
23884
25104
  }, [visibleCaps]);
23885
25105
  const handleSelectNoCaps = (0, react$1.useCallback)(() => {
23886
25106
  setActiveCaps(/* @__PURE__ */ new Set());
23887
25107
  }, []);
25108
+ const toggleNode = (0, react$1.useCallback)((path, currentlyOpen) => {
25109
+ setCollapseOverrides((prev) => {
25110
+ const next = new Map(prev);
25111
+ next.set(path, !currentlyOpen);
25112
+ return next;
25113
+ });
25114
+ }, []);
25115
+ const isPathOpen = (0, react$1.useCallback)((path, depth) => {
25116
+ const override = collapseOverrides.get(path);
25117
+ if (override !== void 0) return override;
25118
+ return depth < DEFAULT_EXPAND_DEPTH;
25119
+ }, [collapseOverrides]);
25120
+ const [copiedAll, setCopiedAll] = (0, react$1.useState)(false);
25121
+ const handleCopyAll = (0, react$1.useCallback)(() => {
25122
+ const out = {};
25123
+ for (const capName of filteredCapNames) {
25124
+ const state = capStates.get(capName);
25125
+ if (state) out[capName] = state.slice;
25126
+ }
25127
+ navigator.clipboard.writeText(JSON.stringify(out, null, 2)).then(() => {
25128
+ setCopiedAll(true);
25129
+ setTimeout(() => setCopiedAll(false), 1500);
25130
+ });
25131
+ }, [filteredCapNames, capStates]);
23888
25132
  const filteredPopoverCaps = (0, react$1.useMemo)(() => {
23889
25133
  const needle = filterSearch.trim().toLowerCase();
23890
25134
  if (!needle) return visibleCaps;
@@ -23903,6 +25147,7 @@ function StateValuesStream({ deviceId, defaultCaps, maxHeight = "max-h-96", limi
23903
25147
  document.addEventListener("mousedown", onDocClick);
23904
25148
  return () => document.removeEventListener("mousedown", onDocClick);
23905
25149
  }, [filterOpen]);
25150
+ const searchNeedle = searchText.trim().toLowerCase();
23906
25151
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
23907
25152
  className: cn("h-full min-h-0 flex flex-col", className),
23908
25153
  onClick: (e) => e.stopPropagation(),
@@ -23919,18 +25164,6 @@ function StateValuesStream({ deviceId, defaultCaps, maxHeight = "max-h-96", limi
23919
25164
  label: "device",
23920
25165
  value: `#${deviceId}`
23921
25166
  }),
23922
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
23923
- type: "button",
23924
- onClick: () => {
23925
- setAutoScroll((a) => !a);
23926
- if (!autoScroll && scrollRef.current) scrollRef.current.scrollTo({
23927
- top: 0,
23928
- behavior: "smooth"
23929
- });
23930
- },
23931
- className: cn("inline-flex items-center gap-1 text-[9px] px-1.5 py-0.5 rounded border transition-colors", autoScroll ? "border-border text-foreground-subtle hover:text-foreground" : "border-amber-500/30 bg-amber-500/10 text-amber-400"),
23932
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ArrowUpToLine, { className: "h-2.5 w-2.5" }), autoScroll ? "Auto" : "Paused"]
23933
- }),
23934
25167
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
23935
25168
  className: "relative",
23936
25169
  ref: filterRootRef,
@@ -23938,7 +25171,7 @@ function StateValuesStream({ deviceId, defaultCaps, maxHeight = "max-h-96", limi
23938
25171
  type: "button",
23939
25172
  onClick: () => setFilterOpen((v) => !v),
23940
25173
  className: cn("inline-flex items-center gap-1 text-[9px] px-1.5 py-0.5 rounded border transition-colors", !allSelected ? "border-violet-500/30 bg-violet-500/10 text-violet-400" : "border-border text-foreground-subtle hover:text-foreground"),
23941
- title: "Pick caps to track",
25174
+ title: "Pick caps to show",
23942
25175
  children: [
23943
25176
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Funnel, { className: "h-2.5 w-2.5" }),
23944
25177
  "Filter (",
@@ -23963,19 +25196,12 @@ function StateValuesStream({ deviceId, defaultCaps, maxHeight = "max-h-96", limi
23963
25196
  title: "Reset filters",
23964
25197
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RotateCcw, { className: "h-2.5 w-2.5" }), "Reset"]
23965
25198
  }),
23966
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
23967
- type: "button",
23968
- onClick: handleClear,
23969
- className: "inline-flex items-center gap-1 text-[9px] px-1.5 py-0.5 rounded border border-border text-foreground-subtle hover:text-foreground transition-colors",
23970
- title: "Clear visible entries",
23971
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Trash2, { className: "h-2.5 w-2.5" }), "Clear"]
23972
- }),
23973
25199
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
23974
25200
  type: "button",
23975
25201
  onClick: handleCopyAll,
23976
- disabled: allEntries.length === 0,
23977
- className: cn("inline-flex items-center gap-1 text-[9px] px-1.5 py-0.5 rounded border transition-colors", copiedAll ? "border-emerald-500/30 bg-emerald-500/10 text-emerald-400" : "border-border text-foreground-subtle hover:text-foreground", allEntries.length === 0 && "opacity-40 cursor-not-allowed"),
23978
- title: "Copy all visible state changes",
25202
+ disabled: filteredCapNames.length === 0,
25203
+ className: cn("inline-flex items-center gap-1 text-[9px] px-1.5 py-0.5 rounded border transition-colors", copiedAll ? "border-emerald-500/30 bg-emerald-500/10 text-emerald-400" : "border-border text-foreground-subtle hover:text-foreground", filteredCapNames.length === 0 && "opacity-40 cursor-not-allowed"),
25204
+ title: "Copy visible state as JSON",
23979
25205
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Copy, { className: "h-2.5 w-2.5" }), copiedAll ? "Copied!" : "Copy"]
23980
25206
  }),
23981
25207
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -23994,13 +25220,9 @@ function StateValuesStream({ deviceId, defaultCaps, maxHeight = "max-h-96", limi
23994
25220
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(X, { className: "h-2.5 w-2.5" })
23995
25221
  })]
23996
25222
  }),
23997
- liveEvents.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
23998
- className: "text-[9px] text-emerald-400 font-medium",
23999
- children: [
24000
- "+",
24001
- liveEvents.length,
24002
- " live"
24003
- ]
25223
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
25224
+ className: "text-[9px] text-emerald-400 font-medium inline-flex items-center gap-1",
25225
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "h-1.5 w-1.5 rounded-full bg-emerald-400 animate-pulse" }), "live"]
24004
25226
  })
24005
25227
  ]
24006
25228
  }), onClose && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
@@ -24010,90 +25232,172 @@ function StateValuesStream({ deviceId, defaultCaps, maxHeight = "max-h-96", limi
24010
25232
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(X, { className: "h-3.5 w-3.5" })
24011
25233
  })]
24012
25234
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
24013
- ref: scrollRef,
24014
- className: cn("flex-1 min-h-0 overflow-auto text-[10px]", maxHeight),
25235
+ className: cn("flex-1 min-h-0 overflow-auto text-[10px] font-mono", maxHeight),
24015
25236
  children: [
24016
- isLoading && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
24017
- className: "flex items-center justify-center gap-2 py-6 text-foreground-subtle",
24018
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LoaderCircle, { className: "h-3.5 w-3.5 animate-spin" }), "Loading state changes..."]
25237
+ isLoading && capStates.size === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
25238
+ className: "flex items-center justify-center gap-2 py-6 text-foreground-subtle font-sans",
25239
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LoaderCircle, { className: "h-3.5 w-3.5 animate-spin" }), "Loading device state..."]
24019
25240
  }),
24020
- !isLoading && allEntries.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
24021
- className: "py-4 text-center text-foreground-subtle",
24022
- children: "No state changes"
25241
+ !isLoading && capStates.size === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
25242
+ className: "py-4 text-center text-foreground-subtle font-sans",
25243
+ children: "No runtime state"
24023
25244
  }),
24024
- allEntries.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
24025
- className: "divide-y divide-border/30",
24026
- children: allEntries.map((entry) => {
24027
- const expanded = expandedRows.has(entry.id);
24028
- const fields = Object.entries(entry.slice);
24029
- const summary = fields.slice(0, 3).map(([k, v]) => `${k}=${formatValue(v)}`).join(" · ");
24030
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
24031
- className: "flex items-start gap-2 px-3 py-1.5 hover:bg-surface-hover/30 cursor-pointer",
24032
- onClick: () => toggleRow(entry.id),
24033
- children: [
24034
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
24035
- className: "text-foreground-subtle whitespace-nowrap w-[70px] shrink-0 pt-0.5",
24036
- children: new Date(entry.timestamp).toLocaleTimeString()
24037
- }),
24038
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RowCopyButton, {
24039
- copied: copiedRowId === entry.id,
24040
- onCopy: () => copyOne(entry)
24041
- }),
24042
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
24043
- className: "flex-1 min-w-0",
24044
- children: [
24045
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
24046
- className: "flex items-center gap-1.5",
24047
- children: [
24048
- expanded ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronDown, { className: "h-3 w-3 text-foreground-subtle shrink-0" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronRight, { className: "h-3 w-3 text-foreground-subtle shrink-0" }),
24049
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
24050
- className: "inline-flex items-center gap-1 rounded-full border border-violet-500/30 bg-violet-500/15 text-violet-400 px-2 py-0.5 text-[9px] font-medium",
24051
- children: entry.capName
24052
- }),
24053
- fields.length > 3 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
24054
- className: "text-[9px] text-foreground-subtle",
24055
- children: [
24056
- "+",
24057
- fields.length - 3,
24058
- " more"
24059
- ]
24060
- })
24061
- ]
24062
- }),
24063
- summary && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
24064
- className: "text-foreground-subtle text-[10px] mt-0.5 ml-5 truncate",
24065
- children: summary
24066
- }),
24067
- expanded && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
24068
- className: "mt-1 ml-5 text-[9px] bg-surface rounded px-2 py-1.5 space-y-0.5 font-mono overflow-x-auto",
24069
- children: fields.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
24070
- className: "text-foreground-subtle italic",
24071
- children: "empty slice"
24072
- }) : fields.map(([k, v]) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
24073
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
24074
- className: "text-primary/70",
24075
- children: k
24076
- }),
24077
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
24078
- className: "text-foreground-subtle",
24079
- children: ": "
24080
- }),
24081
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
24082
- className: "text-foreground break-all",
24083
- children: formatValue(v)
24084
- })
24085
- ] }, k))
24086
- })
24087
- ]
24088
- })
24089
- ]
24090
- }, entry.id);
25245
+ !isLoading && capStates.size > 0 && filteredCapNames.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
25246
+ className: "py-4 text-center text-foreground-subtle font-sans",
25247
+ children: "No caps match the filter"
25248
+ }),
25249
+ filteredCapNames.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
25250
+ className: "py-1",
25251
+ children: filteredCapNames.map((capName) => {
25252
+ const state = capStates.get(capName);
25253
+ if (!state) return null;
25254
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CapNode, {
25255
+ capName,
25256
+ slice: state.slice,
25257
+ updatedAt: state.updatedAt,
25258
+ searchNeedle,
25259
+ isPathOpen,
25260
+ onToggle: toggleNode
25261
+ }, capName);
24091
25262
  })
24092
25263
  })
24093
25264
  ]
24094
25265
  })]
24095
25266
  });
24096
25267
  }
25268
+ /** Top-level tree node: one capability. Header carries the cap-name
25269
+ * pill + an "updated Ns ago" indicator; the body expands to the
25270
+ * slice's keys. */
25271
+ function CapNode({ capName, slice, updatedAt, searchNeedle, isPathOpen, onToggle }) {
25272
+ const path = capName;
25273
+ const open = isPathOpen(path, 0);
25274
+ const entries = Object.entries(slice);
25275
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
25276
+ className: "flex items-center gap-1.5 px-3 py-1 hover:bg-surface-hover/30 cursor-pointer select-none",
25277
+ onClick: () => onToggle(path, open),
25278
+ children: [
25279
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Chevron, {
25280
+ open,
25281
+ hasChildren: entries.length > 0
25282
+ }),
25283
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
25284
+ className: "inline-flex items-center gap-1 rounded-full border border-violet-500/30 bg-violet-500/15 text-violet-400 px-2 py-0.5 text-[9px] font-medium font-sans",
25285
+ children: capName
25286
+ }),
25287
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
25288
+ className: "text-[9px] text-foreground-subtle font-sans",
25289
+ children: [
25290
+ entries.length,
25291
+ " ",
25292
+ entries.length === 1 ? "key" : "keys"
25293
+ ]
25294
+ }),
25295
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
25296
+ className: "ml-auto text-[9px] text-foreground-subtle/70 font-sans tabular-nums",
25297
+ children: ["updated ", formatAge(updatedAt)]
25298
+ })
25299
+ ]
25300
+ }), open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: entries.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LeafEmpty, {
25301
+ depth: 1,
25302
+ label: "empty slice"
25303
+ }) : entries.map(([k, v]) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TreeNode, {
25304
+ path: `${path}.${k}`,
25305
+ nodeKey: k,
25306
+ value: v,
25307
+ depth: 1,
25308
+ searchNeedle,
25309
+ isPathOpen,
25310
+ onToggle
25311
+ }, k)) })] });
25312
+ }
25313
+ /** Recursive tree node. A leaf renders `key: value`; an object or
25314
+ * array renders a collapsible branch whose children recurse one
25315
+ * level deeper. */
25316
+ function TreeNode({ path, nodeKey, value, depth, searchNeedle, isPathOpen, onToggle }) {
25317
+ const branch = asBranch(value);
25318
+ const indentStyle = { paddingLeft: `${12 + depth * 14}px` };
25319
+ if (!branch) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
25320
+ className: "flex items-start gap-1 px-3 py-0.5",
25321
+ style: indentStyle,
25322
+ children: [
25323
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
25324
+ className: cn("shrink-0", highlightCls(nodeKey, searchNeedle, "text-primary/70")),
25325
+ children: nodeKey
25326
+ }),
25327
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
25328
+ className: "text-foreground-subtle",
25329
+ children: ":"
25330
+ }),
25331
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
25332
+ className: cn("break-all", highlightCls(formatValue(value), searchNeedle, valueCls(value))),
25333
+ children: formatValue(value)
25334
+ })
25335
+ ]
25336
+ });
25337
+ const open = isPathOpen(path, depth);
25338
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
25339
+ className: "flex items-center gap-1 px-3 py-0.5 hover:bg-surface-hover/30 cursor-pointer select-none",
25340
+ style: indentStyle,
25341
+ onClick: () => onToggle(path, open),
25342
+ children: [
25343
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Chevron, {
25344
+ open,
25345
+ hasChildren: branch.entries.length > 0
25346
+ }),
25347
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
25348
+ className: highlightCls(nodeKey, searchNeedle, "text-primary/70"),
25349
+ children: nodeKey
25350
+ }),
25351
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
25352
+ className: "text-foreground-subtle/70 text-[9px]",
25353
+ children: branch.summary
25354
+ })
25355
+ ]
25356
+ }), open && (branch.entries.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LeafEmpty, {
25357
+ depth: depth + 1,
25358
+ label: branch.kind === "array" ? "empty array" : "empty object"
25359
+ }) : branch.entries.map(([k, v]) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TreeNode, {
25360
+ path: `${path}.${k}`,
25361
+ nodeKey: k,
25362
+ value: v,
25363
+ depth: depth + 1,
25364
+ searchNeedle,
25365
+ isPathOpen,
25366
+ onToggle
25367
+ }, k)))] });
25368
+ }
25369
+ /** Placeholder row for an object/array/slice with no entries. */
25370
+ function LeafEmpty({ depth, label }) {
25371
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
25372
+ className: "px-3 py-0.5 text-foreground-subtle/60 italic",
25373
+ style: { paddingLeft: `${12 + depth * 14}px` },
25374
+ children: label
25375
+ });
25376
+ }
25377
+ /** Expand/collapse chevron. Renders a fixed-width spacer when the node
25378
+ * has no children so leaf and branch keys stay column-aligned. */
25379
+ function Chevron({ open, hasChildren }) {
25380
+ if (!hasChildren) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "inline-block w-3 shrink-0" });
25381
+ return open ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronDown, { className: "h-3 w-3 text-foreground-subtle shrink-0" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronRight, { className: "h-3 w-3 text-foreground-subtle shrink-0" });
25382
+ }
25383
+ /** Classify a value as an expandable branch (plain object or array)
25384
+ * or `null` for a leaf. Arrays expose index → element entries. */
25385
+ function asBranch(value) {
25386
+ if (Array.isArray(value)) return {
25387
+ kind: "array",
25388
+ entries: value.map((v, i) => [String(i), v]),
25389
+ summary: `[${value.length}]`
25390
+ };
25391
+ if (value && typeof value === "object") {
25392
+ const entries = Object.entries(value);
25393
+ return {
25394
+ kind: "object",
25395
+ entries,
25396
+ summary: `{${entries.length}}`
25397
+ };
25398
+ }
25399
+ return null;
25400
+ }
24097
25401
  function formatValue(v) {
24098
25402
  if (v === null) return "null";
24099
25403
  if (v === void 0) return "undefined";
@@ -24105,9 +25409,46 @@ function formatValue(v) {
24105
25409
  return String(v);
24106
25410
  }
24107
25411
  }
25412
+ /** Tailwind colour for a leaf value by JS type. */
25413
+ function valueCls(v) {
25414
+ if (v === null || v === void 0) return "text-foreground-subtle/60";
25415
+ if (typeof v === "number") return "text-amber-400";
25416
+ if (typeof v === "boolean") return v ? "text-emerald-400" : "text-rose-400";
25417
+ return "text-foreground";
25418
+ }
25419
+ /** Append a highlight background when `text` matches the live search
25420
+ * needle, so search hits are visible inside the tree. */
25421
+ function highlightCls(text, needle, base) {
25422
+ if (needle && text.toLowerCase().includes(needle)) return cn(base, "bg-violet-500/20 rounded px-0.5");
25423
+ return base;
25424
+ }
25425
+ /** Deep search a slice — true when `needle` appears in any key or any
25426
+ * stringified leaf value anywhere in the tree. */
25427
+ function sliceMatchesSearch(value, needle) {
25428
+ if (Array.isArray(value)) return value.some((v) => sliceMatchesSearch(v, needle));
25429
+ if (value && typeof value === "object") {
25430
+ for (const [k, v] of Object.entries(value)) {
25431
+ if (k.toLowerCase().includes(needle)) return true;
25432
+ if (sliceMatchesSearch(v, needle)) return true;
25433
+ }
25434
+ return false;
25435
+ }
25436
+ return formatValue(value).toLowerCase().includes(needle);
25437
+ }
25438
+ /** Human "Ns ago" / "Nm ago" age label for the per-cap freshness
25439
+ * indicator. */
25440
+ function formatAge(updatedAt) {
25441
+ const deltaMs = Date.now() - updatedAt;
25442
+ if (deltaMs < 5e3) return "just now";
25443
+ const secs = Math.floor(deltaMs / 1e3);
25444
+ if (secs < 60) return `${secs}s ago`;
25445
+ const mins = Math.floor(secs / 60);
25446
+ if (mins < 60) return `${mins}m ago`;
25447
+ return `${Math.floor(mins / 60)}h ago`;
25448
+ }
24108
25449
  function ScopeBadge({ label, value }) {
24109
25450
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
24110
- className: "text-[9px] px-1.5 py-0.5 rounded-full bg-primary/10 text-primary font-medium",
25451
+ className: "text-[9px] px-1.5 py-0.5 rounded-full bg-primary/10 text-primary font-medium font-sans",
24111
25452
  children: [
24112
25453
  label,
24113
25454
  ": ",
@@ -24116,16 +25457,13 @@ function ScopeBadge({ label, value }) {
24116
25457
  });
24117
25458
  }
24118
25459
  /**
24119
- * Cap-name multiselect popover — same shape as
24120
- * `CategoryFilterPopover` in event-stream.tsx (search box at top,
24121
- * Select All / None / Defaults buttons, scrollable checkbox list).
24122
- * Caps don't have icons / styled badges like events do — we render
24123
- * a flat name + violet-pill bracket so the look stays consistent
24124
- * with the rest of the StateValuesStream rows.
25460
+ * Cap-name multiselect popover — same shape as `CategoryFilterPopover`
25461
+ * in event-stream.tsx (search box at top, Select All / None / Defaults
25462
+ * buttons, scrollable checkbox list).
24125
25463
  */
24126
25464
  function CapFilterPopover({ search, onSearchChange, caps, active, onToggle, onSelectAll, onSelectNone, onResetDefaults }) {
24127
25465
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
24128
- className: "absolute z-20 top-full mt-1 left-0 w-72 max-h-80 rounded-md border border-border bg-surface shadow-lg flex flex-col",
25466
+ className: "absolute z-20 top-full mt-1 left-0 w-72 max-h-80 rounded-md border border-border bg-surface shadow-lg flex flex-col font-sans",
24129
25467
  onClick: (e) => e.stopPropagation(),
24130
25468
  children: [
24131
25469
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -24196,24 +25534,6 @@ function CapFilterPopover({ search, onSearchChange, caps, active, onToggle, onSe
24196
25534
  ]
24197
25535
  });
24198
25536
  }
24199
- /**
24200
- * Per-row Copy icon — same shape used in log-stream / event-stream
24201
- * so the three live-streams' rows are visually symmetric. Stops
24202
- * propagation so the click doesn't toggle the row's expand state.
24203
- * Flashes a Check for ~1s on success.
24204
- */
24205
- function RowCopyButton({ copied, onCopy }) {
24206
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
24207
- type: "button",
24208
- title: "Copy this state change",
24209
- onClick: (e) => {
24210
- e.stopPropagation();
24211
- onCopy();
24212
- },
24213
- className: cn("rounded p-0.5 transition-colors mt-0.5", copied ? "text-emerald-400" : "text-foreground-subtle/60 hover:text-foreground hover:bg-surface-hover"),
24214
- children: copied ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Check, { className: "h-3 w-3" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Copy, { className: "h-3 w-3" })
24215
- });
24216
- }
24217
25537
  //#endregion
24218
25538
  //#region src/composites/device-activity-panel.tsx
24219
25539
  /**
@@ -24292,303 +25612,99 @@ function DeviceActivityPanel({ deviceId, defaultEventCategories, defaultStateCap
24292
25612
  showFilters: false,
24293
25613
  liveBuffer: logsBuffer
24294
25614
  }),
24295
- tab === "events" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EventStream, {
24296
- deviceId,
24297
- defaultCategories: defaultEventCategories,
24298
- maxHeight,
24299
- showCategoryFilter: true,
24300
- liveBuffer: eventsBuffer
24301
- }),
24302
- tab === "state" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StateValuesStream, {
24303
- deviceId,
24304
- defaultCaps: defaultStateCaps,
24305
- maxHeight,
24306
- liveBuffer: stateBuffer
24307
- })
24308
- ]
24309
- })]
24310
- });
24311
- }
24312
- //#endregion
24313
- //#region src/composites/confirm-action-button.tsx
24314
- /**
24315
- * ConfirmActionButton — destructive-action button with a modal confirm.
24316
- *
24317
- * Two-step UX for any operation operators shouldn't trigger by accident:
24318
- * reboot device, restart addon, regenerate token, etc. The trigger is a
24319
- * normal Button that opens a Dialog; confirming runs the async action,
24320
- * the trigger spinner-disables itself for the duration, and the dialog
24321
- * closes on success. Errors surface inline at the dialog's bottom.
24322
- *
24323
- * Generic on `TResult` so callers don't have to discard the return
24324
- * value. Pass an `icon` to render it inside the trigger (e.g. RotateCw
24325
- * for reboot, RefreshCw for restart).
24326
- */
24327
- function ConfirmActionButton({ label, icon: Icon, title = "Confirm action", description, confirmLabel, triggerVariant = "outline", confirmVariant = "danger", size = "sm", disabled, className, action, onSuccess }) {
24328
- const [open, setOpen] = (0, react$1.useState)(false);
24329
- const [running, setRunning] = (0, react$1.useState)(false);
24330
- const [error, setError] = (0, react$1.useState)(null);
24331
- const handleConfirm = async () => {
24332
- setError(null);
24333
- setRunning(true);
24334
- try {
24335
- const result = await action();
24336
- onSuccess?.(result);
24337
- setOpen(false);
24338
- } catch (err) {
24339
- setError(err instanceof Error ? err.message : String(err));
24340
- } finally {
24341
- setRunning(false);
24342
- }
24343
- };
24344
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
24345
- type: "button",
24346
- variant: triggerVariant,
24347
- size,
24348
- disabled,
24349
- onClick: () => {
24350
- setError(null);
24351
- setOpen(true);
24352
- },
24353
- className,
24354
- children: [Icon && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { className: "h-3.5 w-3.5" }), label]
24355
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Dialog, {
24356
- open,
24357
- onOpenChange: (next) => !running && setOpen(next),
24358
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DialogContent, { children: [
24359
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DialogHeader, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DialogTitle, {
24360
- className: "flex items-center gap-2",
24361
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TriangleAlert, { className: "h-4 w-4 text-amber-400" }), title]
24362
- }), typeof description === "string" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DialogDescription, { children: description }) : description] }),
24363
- error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
24364
- className: cn("mt-2 px-3 py-2 rounded border border-danger/40 bg-danger/10", "text-xs text-danger"),
24365
- children: error
24366
- }),
24367
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DialogFooter, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
24368
- type: "button",
24369
- variant: "ghost",
24370
- size,
24371
- disabled: running,
24372
- onClick: () => setOpen(false),
24373
- children: "Cancel"
24374
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
24375
- type: "button",
24376
- variant: confirmVariant,
24377
- size,
24378
- disabled: running,
24379
- onClick: () => {
24380
- handleConfirm();
24381
- },
24382
- children: [running && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LoaderCircle, { className: "h-3.5 w-3.5 animate-spin" }), confirmLabel ?? label]
24383
- })] })
24384
- ] })
24385
- })] });
24386
- }
24387
- //#endregion
24388
- //#region src/composites/ptz-overlay.tsx
24389
- /**
24390
- * PTZOverlay — pan / tilt / zoom controls.
24391
- *
24392
- * Two visual variants driven by `mode`:
24393
- * - `'overlay'` (default): translucent dark pill positioned bottom-
24394
- * right of the camera viewport. Used as `extraOverlay` on the
24395
- * `CameraStreamPlayer` so operators can drive PTZ without leaving
24396
- * the live view. Opaque background + subtle ring keeps the d-pad
24397
- * legible against any frame.
24398
- * - `'panel'`: full-bleed inside a host container (e.g. the floating
24399
- * PTZ panel in DeviceDetail). No absolute positioning, no inner
24400
- * wrapper card — the host's chrome is the only frame. Inherits the
24401
- * surrounding `bg-surface` so the dark theme reads consistently
24402
- * instead of the previous always-dark-bubble look.
24403
- *
24404
- * Interaction model is identical across modes: short tap fires a
24405
- * discrete pulse (`move`); long press starts continuous motion until
24406
- * release (`startContinuous` + `stopContinuous` on pointer up).
24407
- */
24408
- function DPadButton({ direction, icon: Icon, disabled, className, variant, onMove, onStart, onStop }) {
24409
- const [pressedAt, setPressedAt] = (0, react$1.useState)(null);
24410
- const [continuous, setContinuous] = (0, react$1.useState)(false);
24411
- const handlePointerDown = (0, react$1.useCallback)((e) => {
24412
- if (disabled) return;
24413
- e.currentTarget.setPointerCapture(e.pointerId);
24414
- setPressedAt(Date.now());
24415
- setContinuous(false);
24416
- const timer = setTimeout(() => {
24417
- setContinuous(true);
24418
- onStart(direction);
24419
- }, 250);
24420
- e.currentTarget.dataset.timer = String(timer);
24421
- }, [
24422
- direction,
24423
- disabled,
24424
- onStart
24425
- ]);
24426
- const handlePointerUp = (0, react$1.useCallback)((e) => {
24427
- const timerId = Number(e.currentTarget.dataset.timer);
24428
- if (timerId) clearTimeout(timerId);
24429
- e.currentTarget.dataset.timer = "";
24430
- if (continuous) onStop();
24431
- else if (pressedAt !== null) onMove(direction);
24432
- setPressedAt(null);
24433
- setContinuous(false);
24434
- }, [
24435
- continuous,
24436
- direction,
24437
- onMove,
24438
- onStop,
24439
- pressedAt
24440
- ]);
24441
- const handlePointerCancel = (0, react$1.useCallback)((e) => {
24442
- const timerId = Number(e.currentTarget.dataset.timer);
24443
- if (timerId) clearTimeout(timerId);
24444
- e.currentTarget.dataset.timer = "";
24445
- if (continuous) onStop();
24446
- setPressedAt(null);
24447
- setContinuous(false);
24448
- }, [continuous, onStop]);
24449
- const sizeClass = variant === "panel" ? "h-9 w-9" : "h-7 w-7";
24450
- const iconSizeClass = variant === "panel" ? "h-4 w-4" : "h-3.5 w-3.5";
24451
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
24452
- type: "button",
24453
- disabled,
24454
- onPointerDown: handlePointerDown,
24455
- onPointerUp: handlePointerUp,
24456
- onPointerCancel: handlePointerCancel,
24457
- className: cn("flex items-center justify-center rounded transition-colors disabled:opacity-40", variant === "panel" ? "text-foreground hover:bg-surface-hover active:bg-surface-hover/80" : "text-white hover:bg-white/15 active:bg-white/25", sizeClass, className),
24458
- title: direction,
24459
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { className: iconSizeClass })
24460
- });
24461
- }
24462
- function PTZOverlay({ controls, mode = "overlay", showPresets, showZoom = true, showHome = true, className }) {
24463
- const { move, startContinuous, stopContinuous, zoom, goHome, goToPreset, presets, busy, error } = controls;
24464
- const [presetsOpen, setPresetsOpen] = (0, react$1.useState)(false);
24465
- const presetsVisible = (showPresets ?? presets.length > 0) && presets.length > 0;
24466
- const isPanel = mode === "panel";
24467
- const containerClass = isPanel ? "flex flex-col items-stretch gap-2 w-full h-full p-3" : "pointer-events-auto absolute top-3 right-3 z-10 flex flex-col items-end gap-2";
24468
- const rowClass = isPanel ? cn("flex items-center gap-3 rounded-lg p-2", busy && "ring-1 ring-primary/40") : cn("flex items-center gap-2 rounded-lg border border-white/30 bg-zinc-900/90 backdrop-blur-md p-2 shadow-lg shadow-black/40", busy && "ring-1 ring-primary/40");
24469
- const sideButtonSize = isPanel ? "h-9 w-9" : "h-7 w-7";
24470
- const sideIconSize = isPanel ? "h-4 w-4" : "h-3.5 w-3.5";
24471
- const sideButtonHover = isPanel ? "text-foreground hover:bg-surface-hover" : "text-white hover:bg-white/15";
24472
- const sepClass = isPanel ? "h-12 w-px bg-border mx-1" : "h-12 w-px bg-white/15 mx-1";
24473
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
24474
- className: cn(containerClass, className),
24475
- children: [error && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
24476
- className: "rounded bg-danger/90 px-2 py-1 text-[10px] font-medium text-white shadow-lg max-w-[200px] self-center",
24477
- children: ["PTZ: ", error]
24478
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
24479
- className: cn(rowClass, isPanel && "justify-center"),
24480
- children: [
24481
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
24482
- className: "grid grid-cols-3 gap-0.5",
24483
- children: [
24484
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
24485
- "aria-hidden": true,
24486
- className: isPanel ? "h-9 w-9" : "h-7 w-7"
24487
- }),
24488
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DPadButton, {
24489
- direction: "up",
24490
- icon: ArrowUp,
24491
- variant: mode,
24492
- onMove: move,
24493
- onStart: startContinuous,
24494
- onStop: stopContinuous
24495
- }),
24496
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
24497
- "aria-hidden": true,
24498
- className: isPanel ? "h-9 w-9" : "h-7 w-7"
24499
- }),
24500
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DPadButton, {
24501
- direction: "left",
24502
- icon: ArrowLeft,
24503
- variant: mode,
24504
- onMove: move,
24505
- onStart: startContinuous,
24506
- onStop: stopContinuous
24507
- }),
24508
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
24509
- "aria-hidden": true,
24510
- className: isPanel ? "h-9 w-9" : "h-7 w-7"
24511
- }),
24512
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DPadButton, {
24513
- direction: "right",
24514
- icon: ArrowRight,
24515
- variant: mode,
24516
- onMove: move,
24517
- onStart: startContinuous,
24518
- onStop: stopContinuous
24519
- }),
24520
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
24521
- "aria-hidden": true,
24522
- className: isPanel ? "h-9 w-9" : "h-7 w-7"
24523
- }),
24524
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DPadButton, {
24525
- direction: "down",
24526
- icon: ArrowDown,
24527
- variant: mode,
24528
- onMove: move,
24529
- onStart: startContinuous,
24530
- onStop: stopContinuous
24531
- }),
24532
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
24533
- "aria-hidden": true,
24534
- className: isPanel ? "h-9 w-9" : "h-7 w-7"
24535
- })
24536
- ]
24537
- }),
24538
- (showZoom || showHome) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: sepClass }),
24539
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
24540
- className: "flex flex-col gap-0.5",
24541
- children: [showZoom && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
24542
- type: "button",
24543
- onClick: () => zoom("in"),
24544
- disabled: busy,
24545
- title: "Zoom in",
24546
- className: cn("flex items-center justify-center rounded disabled:opacity-40", sideButtonSize, sideButtonHover),
24547
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZoomIn, { className: sideIconSize })
24548
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
24549
- type: "button",
24550
- onClick: () => zoom("out"),
24551
- disabled: busy,
24552
- title: "Zoom out",
24553
- className: cn("flex items-center justify-center rounded disabled:opacity-40", sideButtonSize, sideButtonHover),
24554
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZoomOut, { className: sideIconSize })
24555
- })] }), showHome && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
24556
- type: "button",
24557
- onClick: () => goHome(),
24558
- disabled: busy,
24559
- title: "Go home",
24560
- className: cn("flex items-center justify-center rounded disabled:opacity-40", sideButtonSize, sideButtonHover),
24561
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(House, { className: sideIconSize })
24562
- })]
25615
+ tab === "events" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EventStream, {
25616
+ deviceId,
25617
+ defaultCategories: defaultEventCategories,
25618
+ maxHeight,
25619
+ showCategoryFilter: true,
25620
+ liveBuffer: eventsBuffer
24563
25621
  }),
24564
- presetsVisible && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
24565
- className: "relative",
24566
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
24567
- type: "button",
24568
- onClick: () => setPresetsOpen((v) => !v),
24569
- disabled: busy,
24570
- className: cn("flex items-center gap-1 rounded px-2 text-[10px] disabled:opacity-40", isPanel ? "h-9 text-foreground hover:bg-surface-hover" : "h-7 text-white hover:bg-white/15"),
24571
- title: "Presets",
24572
- children: ["Presets", /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronDown, { className: cn("h-3 w-3 transition-transform", presetsOpen && "rotate-180") })]
24573
- }), presetsOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
24574
- className: cn("absolute right-0 min-w-[140px] rounded-lg shadow-xl py-1 z-20", isPanel ? "bottom-full mb-1 bg-surface border border-border" : "top-full mt-1 border border-white/30 bg-zinc-900/95 backdrop-blur-md"),
24575
- children: presets.map((p) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
24576
- type: "button",
24577
- onClick: () => {
24578
- goToPreset(p.id);
24579
- setPresetsOpen(false);
24580
- },
24581
- disabled: busy,
24582
- className: cn("block w-full px-3 py-1.5 text-left text-[10px] disabled:opacity-40", isPanel ? "text-foreground hover:bg-surface-hover" : "text-white hover:bg-white/15"),
24583
- children: p.name || p.id
24584
- }, p.id))
24585
- })]
25622
+ tab === "state" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StateValuesStream, {
25623
+ deviceId,
25624
+ defaultCaps: defaultStateCaps,
25625
+ maxHeight,
25626
+ liveBuffer: stateBuffer
24586
25627
  })
24587
25628
  ]
24588
25629
  })]
24589
25630
  });
24590
25631
  }
24591
25632
  //#endregion
25633
+ //#region src/composites/confirm-action-button.tsx
25634
+ /**
25635
+ * ConfirmActionButton — destructive-action button with a modal confirm.
25636
+ *
25637
+ * Two-step UX for any operation operators shouldn't trigger by accident:
25638
+ * reboot device, restart addon, regenerate token, etc. The trigger is a
25639
+ * normal Button that opens a Dialog; confirming runs the async action,
25640
+ * the trigger spinner-disables itself for the duration, and the dialog
25641
+ * closes on success. Errors surface inline at the dialog's bottom.
25642
+ *
25643
+ * Generic on `TResult` so callers don't have to discard the return
25644
+ * value. Pass an `icon` to render it inside the trigger (e.g. RotateCw
25645
+ * for reboot, RefreshCw for restart).
25646
+ */
25647
+ function ConfirmActionButton({ label, icon: Icon, title = "Confirm action", description, confirmLabel, triggerVariant = "outline", confirmVariant = "danger", size = "sm", disabled, className, action, onSuccess }) {
25648
+ const [open, setOpen] = (0, react$1.useState)(false);
25649
+ const [running, setRunning] = (0, react$1.useState)(false);
25650
+ const [error, setError] = (0, react$1.useState)(null);
25651
+ const handleConfirm = async () => {
25652
+ setError(null);
25653
+ setRunning(true);
25654
+ try {
25655
+ const result = await action();
25656
+ onSuccess?.(result);
25657
+ setOpen(false);
25658
+ } catch (err) {
25659
+ setError(err instanceof Error ? err.message : String(err));
25660
+ } finally {
25661
+ setRunning(false);
25662
+ }
25663
+ };
25664
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
25665
+ type: "button",
25666
+ variant: triggerVariant,
25667
+ size,
25668
+ disabled,
25669
+ onClick: () => {
25670
+ setError(null);
25671
+ setOpen(true);
25672
+ },
25673
+ className,
25674
+ children: [Icon && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { className: "h-3.5 w-3.5" }), label]
25675
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Dialog, {
25676
+ open,
25677
+ onOpenChange: (next) => !running && setOpen(next),
25678
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DialogContent, { children: [
25679
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DialogHeader, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DialogTitle, {
25680
+ className: "flex items-center gap-2",
25681
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TriangleAlert, { className: "h-4 w-4 text-amber-400" }), title]
25682
+ }), typeof description === "string" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DialogDescription, { children: description }) : description] }),
25683
+ error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
25684
+ className: cn("mt-2 px-3 py-2 rounded border border-danger/40 bg-danger/10", "text-xs text-danger"),
25685
+ children: error
25686
+ }),
25687
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DialogFooter, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
25688
+ type: "button",
25689
+ variant: "ghost",
25690
+ size,
25691
+ disabled: running,
25692
+ onClick: () => setOpen(false),
25693
+ children: "Cancel"
25694
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
25695
+ type: "button",
25696
+ variant: confirmVariant,
25697
+ size,
25698
+ disabled: running,
25699
+ onClick: () => {
25700
+ handleConfirm();
25701
+ },
25702
+ children: [running && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LoaderCircle, { className: "h-3.5 w-3.5 animate-spin" }), confirmLabel ?? label]
25703
+ })] })
25704
+ ] })
25705
+ })] });
25706
+ }
25707
+ //#endregion
24592
25708
  //#region src/composites/snapshot-button.tsx
24593
25709
  /**
24594
25710
  * SnapshotButton — operator-facing manual snapshot trigger.
@@ -25495,169 +26611,6 @@ function useDeviceWebrtc(trpc, deviceId, pollIntervalMs = 5e3) {
25495
26611
  };
25496
26612
  }
25497
26613
  //#endregion
25498
- //#region src/hooks/use-ptz.ts
25499
- /**
25500
- * usePTZ — PTZ control hook for device-scoped pan / tilt / zoom.
25501
- *
25502
- * Wraps the `ptz` capability methods through the canonical
25503
- * `useDeviceProxy` surface — every call goes through
25504
- * `dev.ptz?.<method>(...)` with deviceId/nodeId auto-injected. The
25505
- * hook stays bare-bones around state (`busy`, `error`, `presets`)
25506
- * so the operator-facing component (PTZOverlay) stays trivial.
25507
- *
25508
- * Returns:
25509
- * - `move(direction)`: discrete one-shot move (cap.move + cap.stop).
25510
- * Use for short pulse moves triggered by tapping a d-pad button.
25511
- * - `startContinuous(direction)` / `stopContinuous()`: gesture-driven
25512
- * continuous motion (`cap.continuousMove` + `cap.stop`). Use for
25513
- * long-press handlers on touch / mouse-down handlers on desktop.
25514
- * - `zoom('in' | 'out')`: discrete zoom step.
25515
- * - `goHome()`: jump to preset 0.
25516
- * - `presets` + `goToPreset(presetId)`: list and jump to named presets.
25517
- *
25518
- * Parametrised by the same `UseDeviceProxyTrpc` shape every other
25519
- * device hook uses — works under admin-ui (BackendClient.trpc) and
25520
- * addon pages (AddonPageProps.trpc) alike.
25521
- */
25522
- var DIRECTION_VECTORS = {
25523
- "up": {
25524
- pan: 0,
25525
- tilt: 1
25526
- },
25527
- "down": {
25528
- pan: 0,
25529
- tilt: -1
25530
- },
25531
- "left": {
25532
- pan: -1,
25533
- tilt: 0
25534
- },
25535
- "right": {
25536
- pan: 1,
25537
- tilt: 0
25538
- },
25539
- "up-left": {
25540
- pan: -1,
25541
- tilt: 1
25542
- },
25543
- "up-right": {
25544
- pan: 1,
25545
- tilt: 1
25546
- },
25547
- "down-left": {
25548
- pan: -1,
25549
- tilt: -1
25550
- },
25551
- "down-right": {
25552
- pan: 1,
25553
- tilt: -1
25554
- }
25555
- };
25556
- function usePTZ(trpc, deviceId, options) {
25557
- const defaultSpeed = options?.defaultSpeed ?? .5;
25558
- const pulseMs = options?.pulseMs ?? 250;
25559
- const enabled = options?.enabled ?? true;
25560
- const ptz = useDeviceProxy(trpc, enabled ? deviceId : null)?.ptz;
25561
- const [presets, setPresets] = (0, react$1.useState)([]);
25562
- const [busy, setBusy] = (0, react$1.useState)(false);
25563
- const [error, setError] = (0, react$1.useState)(null);
25564
- const refreshPresets = (0, react$1.useCallback)(async () => {
25565
- if (!enabled || !ptz) return;
25566
- try {
25567
- setPresets(await ptz.getPresets({}));
25568
- } catch (err) {
25569
- const msg = err instanceof Error ? err.message : String(err);
25570
- if (msg.includes("provider not available") || msg.includes("no 'ptz' binding")) return;
25571
- setError(msg);
25572
- }
25573
- }, [ptz, enabled]);
25574
- (0, react$1.useEffect)(() => {
25575
- refreshPresets();
25576
- }, [refreshPresets]);
25577
- const wrap = (0, react$1.useCallback)(async (fn) => {
25578
- if (!enabled || !ptz) return void 0;
25579
- setError(null);
25580
- setBusy(true);
25581
- try {
25582
- return await fn();
25583
- } catch (err) {
25584
- setError(err instanceof Error ? err.message : String(err));
25585
- return;
25586
- } finally {
25587
- setBusy(false);
25588
- }
25589
- }, [enabled, ptz]);
25590
- return {
25591
- move: (0, react$1.useCallback)(async (direction, speed) => {
25592
- if (!ptz) return;
25593
- const v = DIRECTION_VECTORS[direction];
25594
- const s = speed ?? defaultSpeed;
25595
- await wrap(async () => {
25596
- await ptz.move({
25597
- pan: v.pan,
25598
- tilt: v.tilt,
25599
- speed: s
25600
- });
25601
- await new Promise((r) => setTimeout(r, pulseMs));
25602
- await ptz.stop({});
25603
- });
25604
- }, [
25605
- ptz,
25606
- defaultSpeed,
25607
- pulseMs,
25608
- wrap
25609
- ]),
25610
- startContinuous: (0, react$1.useCallback)(async (direction, speed) => {
25611
- if (!ptz) return;
25612
- const v = DIRECTION_VECTORS[direction];
25613
- const s = speed ?? defaultSpeed;
25614
- await wrap(() => ptz.continuousMove({
25615
- pan: v.pan,
25616
- tilt: v.tilt,
25617
- speed: s
25618
- }));
25619
- }, [
25620
- ptz,
25621
- defaultSpeed,
25622
- wrap
25623
- ]),
25624
- stopContinuous: (0, react$1.useCallback)(async () => {
25625
- if (!ptz) return;
25626
- await wrap(() => ptz.stop({}));
25627
- }, [ptz, wrap]),
25628
- zoom: (0, react$1.useCallback)(async (direction, speed) => {
25629
- if (!ptz) return;
25630
- const z = direction === "in" ? 1 : -1;
25631
- const s = speed ?? defaultSpeed;
25632
- await wrap(async () => {
25633
- await ptz.move({
25634
- zoom: z,
25635
- speed: s
25636
- });
25637
- await new Promise((r) => setTimeout(r, pulseMs));
25638
- await ptz.stop({});
25639
- });
25640
- }, [
25641
- ptz,
25642
- defaultSpeed,
25643
- pulseMs,
25644
- wrap
25645
- ]),
25646
- goHome: (0, react$1.useCallback)(async () => {
25647
- if (!ptz) return;
25648
- await wrap(() => ptz.goHome({}));
25649
- }, [ptz, wrap]),
25650
- goToPreset: (0, react$1.useCallback)(async (presetId) => {
25651
- if (!ptz) return;
25652
- await wrap(() => ptz.goToPreset({ presetId }));
25653
- }, [ptz, wrap]),
25654
- presets,
25655
- refreshPresets,
25656
- busy,
25657
- error
25658
- };
25659
- }
25660
- //#endregion
25661
26614
  //#region src/hooks/use-doorbell-events.ts
25662
26615
  /**
25663
26616
  * useDoorbellEvents — subscribe to doorbell.onPressed across the cluster.
@@ -25885,6 +26838,7 @@ exports.AppShell = AppShell;
25885
26838
  exports.AudioClassificationList = AudioClassificationList;
25886
26839
  exports.AudioLevelWaveform = AudioLevelWaveform;
25887
26840
  exports.AudioWaveform = AudioWaveform;
26841
+ exports.AutotrackSection = AutotrackSection;
25888
26842
  exports.BTN_COMPACT = BTN_COMPACT;
25889
26843
  exports.BTN_COMPACT_DANGER = BTN_COMPACT_DANGER;
25890
26844
  exports.BTN_COMPACT_PRIMARY = BTN_COMPACT_PRIMARY;
@@ -25947,6 +26901,7 @@ exports.FormField = FormField$1;
25947
26901
  exports.GRID_GAP = GRID_GAP;
25948
26902
  exports.GRID_PAIRED = GRID_PAIRED;
25949
26903
  exports.GRID_QUICK_STATS = GRID_QUICK_STATS;
26904
+ exports.HOST_WIDGETS = HOST_WIDGETS;
25950
26905
  exports.INPUT_COMPACT = INPUT_COMPACT;
25951
26906
  exports.IconButton = IconButton;
25952
26907
  exports.ImageSelector = ImageSelector;
@@ -25959,6 +26914,7 @@ exports.Label = Label;
25959
26914
  exports.LogStream = LogStream;
25960
26915
  exports.LoginForm = LoginForm;
25961
26916
  exports.MobileDrawer = MobileDrawer;
26917
+ exports.MotionZonesSettings = MotionZonesSettings;
25962
26918
  exports.NodeMultiSelectField = NodeMultiSelectField;
25963
26919
  exports.NodePicker = NodePicker;
25964
26920
  exports.NodeSelectField = NodeSelectField;
@@ -25975,6 +26931,7 @@ exports.Popover = Popover;
25975
26931
  exports.PopoverContent = PopoverContent;
25976
26932
  exports.PopoverTrigger = PopoverTrigger;
25977
26933
  exports.ProviderBadge = ProviderBadge;
26934
+ exports.PtzPanel = PtzPanel;
25978
26935
  exports.QrCode = QrCode;
25979
26936
  exports.ResponseLog = ResponseLog;
25980
26937
  exports.SECTION_BODY = SECTION_BODY;
@@ -26032,6 +26989,7 @@ exports.getClassColor = getClassColor;
26032
26989
  exports.getPhaseVisual = getPhaseVisual;
26033
26990
  exports.isFieldVisible = isFieldVisible;
26034
26991
  exports.lightColors = require_theme_index.lightColors$1;
26992
+ exports.loadRemoteBundle = loadRemoteBundle;
26035
26993
  exports.mirror = mirror;
26036
26994
  exports.mountAddonPage = mountAddonPage;
26037
26995
  exports.providerIcons = providerIcons;
@@ -26071,6 +27029,7 @@ exports.useAddonsRollbackPackage = useAddonsRollbackPackage;
26071
27029
  exports.useAddonsSearchAvailable = useAddonsSearchAvailable;
26072
27030
  exports.useAddonsSetAddonAutoUpdate = useAddonsSetAddonAutoUpdate;
26073
27031
  exports.useAddonsSetAutoUpdateSettings = useAddonsSetAutoUpdateSettings;
27032
+ exports.useAddonsSetCapabilityProviderEnabled = useAddonsSetCapabilityProviderEnabled;
26074
27033
  exports.useAddonsUninstallPackage = useAddonsUninstallPackage;
26075
27034
  exports.useAddonsUpdateFrameworkPackage = useAddonsUpdateFrameworkPackage;
26076
27035
  exports.useAddonsUpdatePackage = useAddonsUpdatePackage;
@@ -26104,8 +27063,6 @@ exports.useAudioCodecPushEncodedFrame = useAudioCodecPushEncodedFrame;
26104
27063
  exports.useAudioCodecPushPcm = useAudioCodecPushPcm;
26105
27064
  exports.useAudioMetricsGetCurrentSnapshot = useAudioMetricsGetCurrentSnapshot;
26106
27065
  exports.useAudioMetricsGetHistory = useAudioMetricsGetHistory;
26107
- exports.useAuthenticationListProviders = useAuthenticationListProviders;
26108
- exports.useAuthenticationSetProviderEnabled = useAuthenticationSetProviderEnabled;
26109
27066
  exports.useBackupDelete = useBackupDelete;
26110
27067
  exports.useBackupGetEntries = useBackupGetEntries;
26111
27068
  exports.useBackupList = useBackupList;
@@ -26130,11 +27087,14 @@ exports.useCustomFieldRenderer = useCustomFieldRenderer;
26130
27087
  exports.useDebouncedString = useDebouncedString;
26131
27088
  exports.useDecoderCreateSession = useDecoderCreateSession;
26132
27089
  exports.useDecoderDestroySession = useDecoderDestroySession;
27090
+ exports.useDecoderGetFrame = useDecoderGetFrame;
26133
27091
  exports.useDecoderGetInfo = useDecoderGetInfo;
27092
+ exports.useDecoderGetShmStats = useDecoderGetShmStats;
26134
27093
  exports.useDecoderGetStats = useDecoderGetStats;
26135
27094
  exports.useDecoderListActiveSessions = useDecoderListActiveSessions;
26136
27095
  exports.useDecoderOpenStream = useDecoderOpenStream;
26137
27096
  exports.useDecoderPullFrames = useDecoderPullFrames;
27097
+ exports.useDecoderPullHandles = useDecoderPullHandles;
26138
27098
  exports.useDecoderPushPacket = useDecoderPushPacket;
26139
27099
  exports.useDecoderReprobeHwaccel = useDecoderReprobeHwaccel;
26140
27100
  exports.useDecoderSupportsCodec = useDecoderSupportsCodec;
@@ -26144,6 +27104,7 @@ exports.useDetectionPipelineGetDeviceLiveContribution = useDetectionPipelineGetD
26144
27104
  exports.useDetectionPipelineGetDeviceSettingsContribution = useDetectionPipelineGetDeviceSettingsContribution;
26145
27105
  exports.useDevShell = useDevShell;
26146
27106
  exports.useDevice = useDevice;
27107
+ exports.useDeviceAutotrack = useDeviceAutotrack;
26147
27108
  exports.useDeviceBattery = useDeviceBattery;
26148
27109
  exports.useDeviceCapability = useDeviceCapability;
26149
27110
  exports.useDeviceDetections = useDeviceDetections;
@@ -26277,12 +27238,6 @@ exports.useMeshNetworkListPeers = useMeshNetworkListPeers;
26277
27238
  exports.useMeshNetworkLogout = useMeshNetworkLogout;
26278
27239
  exports.useMeshNetworkStartLogin = useMeshNetworkStartLogin;
26279
27240
  exports.useMeshNetworkTestConnection = useMeshNetworkTestConnection;
26280
- exports.useMeshOrchestratorJoinProvider = useMeshOrchestratorJoinProvider;
26281
- exports.useMeshOrchestratorLeaveProvider = useMeshOrchestratorLeaveProvider;
26282
- exports.useMeshOrchestratorListProviderPeers = useMeshOrchestratorListProviderPeers;
26283
- exports.useMeshOrchestratorListProviders = useMeshOrchestratorListProviders;
26284
- exports.useMeshOrchestratorLogoutProvider = useMeshOrchestratorLogoutProvider;
26285
- exports.useMeshOrchestratorStartLoginProvider = useMeshOrchestratorStartLoginProvider;
26286
27241
  exports.useMetricsProviderCollectSnapshot = useMetricsProviderCollectSnapshot;
26287
27242
  exports.useMetricsProviderGetAddonStats = useMetricsProviderGetAddonStats;
26288
27243
  exports.useMetricsProviderGetCached = useMetricsProviderGetCached;
@@ -26304,6 +27259,9 @@ exports.useMotionGetStatus = useMotionGetStatus;
26304
27259
  exports.useMotionIsDetected = useMotionIsDetected;
26305
27260
  exports.useMotionTriggerGetStatus = useMotionTriggerGetStatus;
26306
27261
  exports.useMotionTriggerSetMotionTrigger = useMotionTriggerSetMotionTrigger;
27262
+ exports.useMotionZonesGetOptions = useMotionZonesGetOptions;
27263
+ exports.useMotionZonesGetStatus = useMotionZonesGetStatus;
27264
+ exports.useMotionZonesSetZone = useMotionZonesSetZone;
26307
27265
  exports.useMqttBrokerAddBroker = useMqttBrokerAddBroker;
26308
27266
  exports.useMqttBrokerGetBrokerConfig = useMqttBrokerGetBrokerConfig;
26309
27267
  exports.useMqttBrokerGetStatus = useMqttBrokerGetStatus;
@@ -26313,12 +27271,18 @@ exports.useMqttBrokerStartEmbeddedBroker = useMqttBrokerStartEmbeddedBroker;
26313
27271
  exports.useMqttBrokerStopEmbeddedBroker = useMqttBrokerStopEmbeddedBroker;
26314
27272
  exports.useMqttBrokerTestConnection = useMqttBrokerTestConnection;
26315
27273
  exports.useNativeObjectDetectionGetStatus = useNativeObjectDetectionGetStatus;
27274
+ exports.useNetworkAccessGetEndpoint = useNetworkAccessGetEndpoint;
27275
+ exports.useNetworkAccessGetStatus = useNetworkAccessGetStatus;
27276
+ exports.useNetworkAccessListEndpoints = useNetworkAccessListEndpoints;
27277
+ exports.useNetworkAccessStart = useNetworkAccessStart;
27278
+ exports.useNetworkAccessStop = useNetworkAccessStop;
26316
27279
  exports.useNetworkQualityGetAllStats = useNetworkQualityGetAllStats;
26317
27280
  exports.useNetworkQualityGetDeviceStats = useNetworkQualityGetDeviceStats;
26318
27281
  exports.useNetworkQualityReportClientStats = useNetworkQualityReportClientStats;
26319
27282
  exports.useNodesClusterAddonStatus = useNodesClusterAddonStatus;
26320
27283
  exports.useNodesDeployAddon = useNodesDeployAddon;
26321
27284
  exports.useNodesExecuteQuery = useNodesExecuteQuery;
27285
+ exports.useNodesGetCapUsageGraph = useNodesGetCapUsageGraph;
26322
27286
  exports.useNodesGetNodeAddons = useNodesGetNodeAddons;
26323
27287
  exports.useNodesRenameNode = useNodesRenameNode;
26324
27288
  exports.useNodesRestartAddon = useNodesRestartAddon;
@@ -26441,12 +27405,16 @@ exports.usePtzAutotrackGetStatus = usePtzAutotrackGetStatus;
26441
27405
  exports.usePtzAutotrackSetEnabled = usePtzAutotrackSetEnabled;
26442
27406
  exports.usePtzAutotrackSetSettings = usePtzAutotrackSetSettings;
26443
27407
  exports.usePtzContinuousMove = usePtzContinuousMove;
27408
+ exports.usePtzDeletePreset = usePtzDeletePreset;
27409
+ exports.usePtzGetOptions = usePtzGetOptions;
26444
27410
  exports.usePtzGetPosition = usePtzGetPosition;
26445
27411
  exports.usePtzGetPresets = usePtzGetPresets;
26446
27412
  exports.usePtzGetStatus = usePtzGetStatus;
26447
27413
  exports.usePtzGoHome = usePtzGoHome;
26448
27414
  exports.usePtzGoToPreset = usePtzGoToPreset;
26449
27415
  exports.usePtzMove = usePtzMove;
27416
+ exports.usePtzSavePreset = usePtzSavePreset;
27417
+ exports.usePtzSetAutofocus = usePtzSetAutofocus;
26450
27418
  exports.usePtzStop = usePtzStop;
26451
27419
  exports.useRebootReboot = useRebootReboot;
26452
27420
  exports.useRecordingEngineDisable = useRecordingEngineDisable;
@@ -26470,9 +27438,7 @@ exports.useRecordingEngineUpdateRetentionConfig = useRecordingEngineUpdateRetent
26470
27438
  exports.useRecordingGetPlaybackUrl = useRecordingGetPlaybackUrl;
26471
27439
  exports.useRecordingGetSegments = useRecordingGetSegments;
26472
27440
  exports.useRecordingGetThumbnailAt = useRecordingGetThumbnailAt;
26473
- exports.useRemoteAccessListProviders = useRemoteAccessListProviders;
26474
- exports.useRemoteAccessStartProvider = useRemoteAccessStartProvider;
26475
- exports.useRemoteAccessStopProvider = useRemoteAccessStopProvider;
27441
+ exports.useRemoteComponent = useRemoteComponent;
26476
27442
  exports.useSettingsStoreCount = useSettingsStoreCount;
26477
27443
  exports.useSettingsStoreDeclareCollection = useSettingsStoreDeclareCollection;
26478
27444
  exports.useSettingsStoreDelete = useSettingsStoreDelete;
@@ -26514,7 +27480,6 @@ exports.useStorageWriteChunk = useStorageWriteChunk;
26514
27480
  exports.useStreamBrokerApplyDeviceSettingsPatch = useStreamBrokerApplyDeviceSettingsPatch;
26515
27481
  exports.useStreamBrokerAssignProfile = useStreamBrokerAssignProfile;
26516
27482
  exports.useStreamBrokerGetAllRtspEntries = useStreamBrokerGetAllRtspEntries;
26517
- exports.useStreamBrokerGetBroker = useStreamBrokerGetBroker;
26518
27483
  exports.useStreamBrokerGetBrokerStats = useStreamBrokerGetBrokerStats;
26519
27484
  exports.useStreamBrokerGetDeviceLiveContribution = useStreamBrokerGetDeviceLiveContribution;
26520
27485
  exports.useStreamBrokerGetDeviceSettingsContribution = useStreamBrokerGetDeviceSettingsContribution;
@@ -26529,13 +27494,23 @@ exports.useStreamBrokerListAllCameraStreams = useStreamBrokerListAllCameraStream
26529
27494
  exports.useStreamBrokerListAllProfileSlots = useStreamBrokerListAllProfileSlots;
26530
27495
  exports.useStreamBrokerListClients = useStreamBrokerListClients;
26531
27496
  exports.useStreamBrokerPublishCameraStream = useStreamBrokerPublishCameraStream;
27497
+ exports.useStreamBrokerPullAudioChunks = useStreamBrokerPullAudioChunks;
27498
+ exports.useStreamBrokerPullFrameHandles = useStreamBrokerPullFrameHandles;
26532
27499
  exports.useStreamBrokerRegenerateRtspToken = useStreamBrokerRegenerateRtspToken;
26533
27500
  exports.useStreamBrokerReleaseStreamWithCodec = useStreamBrokerReleaseStreamWithCodec;
26534
27501
  exports.useStreamBrokerRestartProfile = useStreamBrokerRestartProfile;
26535
27502
  exports.useStreamBrokerRetractCameraStream = useStreamBrokerRetractCameraStream;
26536
27503
  exports.useStreamBrokerSetPreBufferDuration = useStreamBrokerSetPreBufferDuration;
26537
27504
  exports.useStreamBrokerSetRtspEnabled = useStreamBrokerSetRtspEnabled;
27505
+ exports.useStreamBrokerSubscribeAudioChunks = useStreamBrokerSubscribeAudioChunks;
27506
+ exports.useStreamBrokerSubscribeFrames = useStreamBrokerSubscribeFrames;
26538
27507
  exports.useStreamBrokerUnassignProfile = useStreamBrokerUnassignProfile;
27508
+ exports.useStreamBrokerUnsubscribeAudioChunks = useStreamBrokerUnsubscribeAudioChunks;
27509
+ exports.useStreamBrokerUnsubscribeFrames = useStreamBrokerUnsubscribeFrames;
27510
+ exports.useStreamParamsGetConfigSchema = useStreamParamsGetConfigSchema;
27511
+ exports.useStreamParamsGetOptions = useStreamParamsGetOptions;
27512
+ exports.useStreamParamsGetStatus = useStreamParamsGetStatus;
27513
+ exports.useStreamParamsSetProfile = useStreamParamsSetProfile;
26539
27514
  exports.useSwitchGetStatus = useSwitchGetStatus;
26540
27515
  exports.useSwitchSetState = useSwitchSetState;
26541
27516
  exports.useSystem = useSystem;
@@ -26550,9 +27525,6 @@ exports.useSystemQuery = useSystemQuery;
26550
27525
  exports.useSystemSetRetentionConfig = useSystemSetRetentionConfig;
26551
27526
  exports.useThemeMode = require_theme_index.useThemeMode$1;
26552
27527
  exports.useToastOnToast = useToastOnToast;
26553
- exports.useTurnOrchestratorGetAllServers = useTurnOrchestratorGetAllServers;
26554
- exports.useTurnOrchestratorListProviders = useTurnOrchestratorListProviders;
26555
- exports.useTurnOrchestratorSetProviderEnabled = useTurnOrchestratorSetProviderEnabled;
26556
27528
  exports.useTurnProviderGetTurnServers = useTurnProviderGetTurnServers;
26557
27529
  exports.useUserManagementConfirmTotp = useUserManagementConfirmTotp;
26558
27530
  exports.useUserManagementCreateApiKey = useUserManagementCreateApiKey;
@@ -26562,10 +27534,16 @@ exports.useUserManagementDeleteUser = useUserManagementDeleteUser;
26562
27534
  exports.useUserManagementDisableTotp = useUserManagementDisableTotp;
26563
27535
  exports.useUserManagementGetTotpStatus = useUserManagementGetTotpStatus;
26564
27536
  exports.useUserManagementListApiKeys = useUserManagementListApiKeys;
27537
+ exports.useUserManagementListOauthSessions = useUserManagementListOauthSessions;
26565
27538
  exports.useUserManagementListScopedTokens = useUserManagementListScopedTokens;
26566
27539
  exports.useUserManagementListUsers = useUserManagementListUsers;
27540
+ exports.useUserManagementOauthExchangeCode = useUserManagementOauthExchangeCode;
27541
+ exports.useUserManagementOauthIssueCode = useUserManagementOauthIssueCode;
27542
+ exports.useUserManagementOauthRefresh = useUserManagementOauthRefresh;
27543
+ exports.useUserManagementOauthVerifyAccessToken = useUserManagementOauthVerifyAccessToken;
26567
27544
  exports.useUserManagementResetPassword = useUserManagementResetPassword;
26568
27545
  exports.useUserManagementRevokeApiKey = useUserManagementRevokeApiKey;
27546
+ exports.useUserManagementRevokeOauthSession = useUserManagementRevokeOauthSession;
26569
27547
  exports.useUserManagementRevokeScopedToken = useUserManagementRevokeScopedToken;
26570
27548
  exports.useUserManagementSetUserScopes = useUserManagementSetUserScopes;
26571
27549
  exports.useUserManagementSetupTotp = useUserManagementSetupTotp;