@camstack/ui-library 1.2.248 → 1.2.250

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
@@ -25,13 +25,13 @@ const require_theme_index = require("./theme/index.cjs");
25
25
  let react = require("react");
26
26
  let react$1 = __toESM(react, 1);
27
27
  react = __toESM(react);
28
+ let _camstack_types = require("@camstack/types");
28
29
  let _trpc_react_query = require("@trpc/react-query");
29
30
  _trpc_react_query = __toESM(_trpc_react_query, 1);
30
31
  let react_jsx_runtime = require("react/jsx-runtime");
31
32
  react_jsx_runtime = __toESM(react_jsx_runtime, 1);
32
33
  let react_dom = require("react-dom");
33
34
  react_dom = __toESM(react_dom, 1);
34
- let _camstack_types = require("@camstack/types");
35
35
  let _tanstack_react_query = require("@tanstack/react-query");
36
36
  _tanstack_react_query = __toESM(_tanstack_react_query, 1);
37
37
  let react_dom_client = require("react-dom/client");
@@ -2454,6 +2454,26 @@ var Gauge = createLucideIcon("gauge", [["path", {
2454
2454
  * This source code is licensed under the ISC license.
2455
2455
  * See the LICENSE file in the root directory of this source tree.
2456
2456
  */
2457
+ var GraduationCap = createLucideIcon("graduation-cap", [
2458
+ ["path", {
2459
+ d: "M21.42 10.922a1 1 0 0 0-.019-1.838L12.83 5.18a2 2 0 0 0-1.66 0L2.6 9.08a1 1 0 0 0 0 1.832l8.57 3.908a2 2 0 0 0 1.66 0z",
2460
+ key: "j76jl0"
2461
+ }],
2462
+ ["path", {
2463
+ d: "M22 10v6",
2464
+ key: "1lu8f3"
2465
+ }],
2466
+ ["path", {
2467
+ d: "M6 12.5V16a6 3 0 0 0 12 0v-3.5",
2468
+ key: "1r8lef"
2469
+ }]
2470
+ ]);
2471
+ /**
2472
+ * @license lucide-react v0.576.0 - ISC
2473
+ *
2474
+ * This source code is licensed under the ISC license.
2475
+ * See the LICENSE file in the root directory of this source tree.
2476
+ */
2457
2477
  var Grid2x2 = createLucideIcon("grid-2x2", [
2458
2478
  ["path", {
2459
2479
  d: "M12 3v18",
@@ -6466,138 +6486,1061 @@ function useClusterNodes() {
6466
6486
  };
6467
6487
  }
6468
6488
  //#endregion
6469
- //#region src/generated/system-hooks.ts
6489
+ //#region src/composites/device-meta.ts
6490
+ var DEVICE_TYPE_META = {
6491
+ [_camstack_types.DeviceType.Camera]: {
6492
+ icon: Camera,
6493
+ label: "Camera"
6494
+ },
6495
+ [_camstack_types.DeviceType.Hub]: {
6496
+ icon: Network,
6497
+ label: "Hub"
6498
+ },
6499
+ [_camstack_types.DeviceType.Light]: {
6500
+ icon: Lightbulb,
6501
+ label: "Light"
6502
+ },
6503
+ [_camstack_types.DeviceType.Siren]: {
6504
+ icon: Siren,
6505
+ label: "Siren"
6506
+ },
6507
+ [_camstack_types.DeviceType.Switch]: {
6508
+ icon: ToggleLeft,
6509
+ label: "Switch"
6510
+ },
6511
+ [_camstack_types.DeviceType.Sensor]: {
6512
+ icon: Activity,
6513
+ label: "Sensor"
6514
+ },
6515
+ [_camstack_types.DeviceType.Thermostat]: {
6516
+ icon: Thermometer,
6517
+ label: "Thermostat"
6518
+ },
6519
+ [_camstack_types.DeviceType.Climate]: {
6520
+ icon: AirVent,
6521
+ label: "Air conditioner"
6522
+ },
6523
+ [_camstack_types.DeviceType.Button]: {
6524
+ icon: CircleDot,
6525
+ label: "Button"
6526
+ },
6527
+ [_camstack_types.DeviceType.EventEmitter]: {
6528
+ icon: Zap,
6529
+ label: "Event Emitter"
6530
+ },
6531
+ [_camstack_types.DeviceType.Update]: {
6532
+ icon: RefreshCw,
6533
+ label: "Update"
6534
+ },
6535
+ [_camstack_types.DeviceType.Generic]: {
6536
+ icon: Box,
6537
+ label: "Generic"
6538
+ },
6539
+ [_camstack_types.DeviceType.Notifier]: {
6540
+ icon: Bell,
6541
+ label: "Notifier"
6542
+ },
6543
+ [_camstack_types.DeviceType.Script]: {
6544
+ icon: ScrollText,
6545
+ label: "Script"
6546
+ },
6547
+ [_camstack_types.DeviceType.Automation]: {
6548
+ icon: Workflow,
6549
+ label: "Automation"
6550
+ },
6551
+ [_camstack_types.DeviceType.Lock]: {
6552
+ icon: Lock,
6553
+ label: "Lock"
6554
+ },
6555
+ [_camstack_types.DeviceType.Cover]: {
6556
+ icon: Blinds,
6557
+ label: "Cover"
6558
+ },
6559
+ [_camstack_types.DeviceType.Valve]: {
6560
+ icon: Pipette,
6561
+ label: "Valve"
6562
+ },
6563
+ [_camstack_types.DeviceType.Humidifier]: {
6564
+ icon: Waves,
6565
+ label: "Humidifier"
6566
+ },
6567
+ [_camstack_types.DeviceType.WaterHeater]: {
6568
+ icon: Bath,
6569
+ label: "Water heater"
6570
+ },
6571
+ [_camstack_types.DeviceType.Fan]: {
6572
+ icon: Fan,
6573
+ label: "Fan"
6574
+ },
6575
+ [_camstack_types.DeviceType.MediaPlayer]: {
6576
+ icon: MonitorPlay,
6577
+ label: "Media player"
6578
+ },
6579
+ [_camstack_types.DeviceType.AlarmPanel]: {
6580
+ icon: ShieldAlert,
6581
+ label: "Alarm panel"
6582
+ },
6583
+ [_camstack_types.DeviceType.Control]: {
6584
+ icon: SlidersHorizontal,
6585
+ label: "Control"
6586
+ },
6587
+ [_camstack_types.DeviceType.Presence]: {
6588
+ icon: MapPin,
6589
+ label: "Presence"
6590
+ },
6591
+ [_camstack_types.DeviceType.Weather]: {
6592
+ icon: CloudSun,
6593
+ label: "Weather"
6594
+ },
6595
+ [_camstack_types.DeviceType.Vacuum]: {
6596
+ icon: Bot,
6597
+ label: "Vacuum"
6598
+ },
6599
+ [_camstack_types.DeviceType.LawnMower]: {
6600
+ icon: Tractor,
6601
+ label: "Lawn mower"
6602
+ },
6603
+ [_camstack_types.DeviceType.Container]: {
6604
+ icon: Box,
6605
+ label: "Container"
6606
+ },
6607
+ [_camstack_types.DeviceType.Image]: {
6608
+ icon: Image,
6609
+ label: "Image"
6610
+ },
6611
+ [_camstack_types.DeviceType.PetFeeder]: {
6612
+ icon: Bone,
6613
+ label: "Pet feeder"
6614
+ }
6615
+ };
6616
+ var DEVICE_ROLE_META = {
6617
+ [_camstack_types.DeviceRole.Siren]: {
6618
+ icon: Siren,
6619
+ label: "Siren"
6620
+ },
6621
+ [_camstack_types.DeviceRole.Floodlight]: {
6622
+ icon: Lightbulb,
6623
+ label: "Floodlight"
6624
+ },
6625
+ [_camstack_types.DeviceRole.Spotlight]: {
6626
+ icon: Flashlight,
6627
+ label: "Spotlight"
6628
+ },
6629
+ [_camstack_types.DeviceRole.PirSensor]: {
6630
+ icon: Radar,
6631
+ label: "PIR sensor"
6632
+ },
6633
+ [_camstack_types.DeviceRole.Chime]: {
6634
+ icon: Bell,
6635
+ label: "Chime"
6636
+ },
6637
+ [_camstack_types.DeviceRole.Autotrack]: {
6638
+ icon: Crosshair,
6639
+ label: "Auto-track"
6640
+ },
6641
+ [_camstack_types.DeviceRole.Nightvision]: {
6642
+ icon: Moon,
6643
+ label: "Night vision"
6644
+ },
6645
+ [_camstack_types.DeviceRole.PrivacyMask]: {
6646
+ icon: EyeOff,
6647
+ label: "Privacy mask"
6648
+ },
6649
+ [_camstack_types.DeviceRole.Doorbell]: {
6650
+ icon: DoorClosed,
6651
+ label: "Doorbell"
6652
+ },
6653
+ [_camstack_types.DeviceRole.BinaryHelper]: {
6654
+ icon: ToggleLeft,
6655
+ label: "Binary helper"
6656
+ },
6657
+ [_camstack_types.DeviceRole.MotionSensor]: {
6658
+ icon: Footprints,
6659
+ label: "Motion sensor"
6660
+ },
6661
+ [_camstack_types.DeviceRole.ContactSensor]: {
6662
+ icon: DoorOpen,
6663
+ label: "Contact sensor"
6664
+ },
6665
+ [_camstack_types.DeviceRole.LeakSensor]: {
6666
+ icon: Droplet,
6667
+ label: "Leak sensor"
6668
+ },
6669
+ [_camstack_types.DeviceRole.SmokeSensor]: {
6670
+ icon: AlarmSmoke,
6671
+ label: "Smoke sensor"
6672
+ },
6673
+ [_camstack_types.DeviceRole.COSensor]: {
6674
+ icon: Wind,
6675
+ label: "CO sensor"
6676
+ },
6677
+ [_camstack_types.DeviceRole.GasSensor]: {
6678
+ icon: Flame,
6679
+ label: "Gas sensor"
6680
+ },
6681
+ [_camstack_types.DeviceRole.TamperSensor]: {
6682
+ icon: ShieldAlert,
6683
+ label: "Tamper sensor"
6684
+ },
6685
+ [_camstack_types.DeviceRole.VibrationSensor]: {
6686
+ icon: Vibrate,
6687
+ label: "Vibration sensor"
6688
+ },
6689
+ [_camstack_types.DeviceRole.ConnectivitySensor]: {
6690
+ icon: Wifi,
6691
+ label: "Connectivity"
6692
+ },
6693
+ [_camstack_types.DeviceRole.SoundSensor]: {
6694
+ icon: Volume2,
6695
+ label: "Sound sensor"
6696
+ },
6697
+ [_camstack_types.DeviceRole.BinarySensor]: {
6698
+ icon: ToggleLeft,
6699
+ label: "Binary sensor"
6700
+ },
6701
+ [_camstack_types.DeviceRole.TemperatureSensor]: {
6702
+ icon: Thermometer,
6703
+ label: "Temperature"
6704
+ },
6705
+ [_camstack_types.DeviceRole.HumiditySensor]: {
6706
+ icon: Droplets,
6707
+ label: "Humidity"
6708
+ },
6709
+ [_camstack_types.DeviceRole.AmbientLightSensor]: {
6710
+ icon: Sun,
6711
+ label: "Ambient light"
6712
+ },
6713
+ [_camstack_types.DeviceRole.PressureSensor]: {
6714
+ icon: Gauge,
6715
+ label: "Pressure"
6716
+ },
6717
+ [_camstack_types.DeviceRole.WindSensor]: {
6718
+ icon: Wind,
6719
+ label: "Wind"
6720
+ },
6721
+ [_camstack_types.DeviceRole.RainSensor]: {
6722
+ icon: CloudRain,
6723
+ label: "Rain"
6724
+ },
6725
+ [_camstack_types.DeviceRole.UvSensor]: {
6726
+ icon: SunMedium,
6727
+ label: "UV index"
6728
+ },
6729
+ [_camstack_types.DeviceRole.SolarRadiationSensor]: {
6730
+ icon: Sunrise,
6731
+ label: "Solar radiation"
6732
+ },
6733
+ [_camstack_types.DeviceRole.SoilMoistureSensor]: {
6734
+ icon: Sprout,
6735
+ label: "Soil moisture"
6736
+ },
6737
+ [_camstack_types.DeviceRole.PowerSensor]: {
6738
+ icon: Zap,
6739
+ label: "Power"
6740
+ },
6741
+ [_camstack_types.DeviceRole.EnergySensor]: {
6742
+ icon: Zap,
6743
+ label: "Energy"
6744
+ },
6745
+ [_camstack_types.DeviceRole.VoltageSensor]: {
6746
+ icon: Zap,
6747
+ label: "Voltage"
6748
+ },
6749
+ [_camstack_types.DeviceRole.CurrentSensor]: {
6750
+ icon: Zap,
6751
+ label: "Current"
6752
+ },
6753
+ [_camstack_types.DeviceRole.AirQualitySensor]: {
6754
+ icon: Wind,
6755
+ label: "Air quality"
6756
+ },
6757
+ [_camstack_types.DeviceRole.BatterySensor]: {
6758
+ icon: Gauge,
6759
+ label: "Battery"
6760
+ },
6761
+ [_camstack_types.DeviceRole.NumericSensor]: {
6762
+ icon: Hash,
6763
+ label: "Numeric sensor"
6764
+ },
6765
+ [_camstack_types.DeviceRole.EnumSensor]: {
6766
+ icon: List,
6767
+ label: "Enum sensor"
6768
+ },
6769
+ [_camstack_types.DeviceRole.DateTimeSensor]: {
6770
+ icon: Clock,
6771
+ label: "Date / time"
6772
+ },
6773
+ [_camstack_types.DeviceRole.GenericSensor]: {
6774
+ icon: Gauge,
6775
+ label: "Sensor"
6776
+ },
6777
+ [_camstack_types.DeviceRole.NumericControl]: {
6778
+ icon: SlidersHorizontal,
6779
+ label: "Numeric control"
6780
+ },
6781
+ [_camstack_types.DeviceRole.SelectControl]: {
6782
+ icon: ListChecks,
6783
+ label: "Select control"
6784
+ },
6785
+ [_camstack_types.DeviceRole.TextControl]: {
6786
+ icon: Type,
6787
+ label: "Text control"
6788
+ },
6789
+ [_camstack_types.DeviceRole.DateTimeControl]: {
6790
+ icon: Clock,
6791
+ label: "Date / time control"
6792
+ },
6793
+ [_camstack_types.DeviceRole.MobilePushNotifier]: {
6794
+ icon: Smartphone,
6795
+ label: "Mobile push"
6796
+ },
6797
+ [_camstack_types.DeviceRole.MessagingNotifier]: {
6798
+ icon: MessageSquare,
6799
+ label: "Messaging"
6800
+ },
6801
+ [_camstack_types.DeviceRole.EmailNotifier]: {
6802
+ icon: Mail,
6803
+ label: "Email"
6804
+ },
6805
+ [_camstack_types.DeviceRole.GenericNotifier]: {
6806
+ icon: Bell,
6807
+ label: "Notifier"
6808
+ }
6809
+ };
6810
+ var DEVICE_TYPE_VALUES$1 = new Set(Object.values(_camstack_types.DeviceType));
6811
+ var DEVICE_ROLE_VALUES = new Set(Object.values(_camstack_types.DeviceRole));
6812
+ function isDeviceType(value) {
6813
+ return DEVICE_TYPE_VALUES$1.has(value);
6814
+ }
6815
+ function isDeviceRole(value) {
6816
+ return DEVICE_ROLE_VALUES.has(value);
6817
+ }
6818
+ /** Meta for a known `DeviceType`. */
6819
+ function deviceTypeMeta(type) {
6820
+ return DEVICE_TYPE_META[type];
6821
+ }
6822
+ /** Meta for a known `DeviceRole`. */
6823
+ function deviceRoleMeta(role) {
6824
+ return DEVICE_ROLE_META[role];
6825
+ }
6826
+ var FALLBACK_DEVICE_TYPE_META = DEVICE_TYPE_META[_camstack_types.DeviceType.Generic];
6827
+ var FALLBACK_DEVICE_ROLE_META = DEVICE_ROLE_META[_camstack_types.DeviceRole.GenericSensor];
6470
6828
  /**
6471
- * Per-method React-Query hooks for every `scope: 'system'` capability.
6829
+ * Runtime-string-safe lookup: device data crosses process / network boundaries
6830
+ * as plain strings, so an unrecognised value falls back to the `Generic` meta
6831
+ * instead of throwing.
6472
6832
  *
6473
- * Each hook is a thin pass-through to `trpc.<cap>.<method>.useQuery` /
6474
- * `.useMutation` / `.useSubscription` so the typed inference + RQ
6475
- * cache/dedup keep working — the generator only contributes a consistent
6476
- * name and stable export surface so admin-ui pages can write
6477
- * `useAddonsList()` instead of `trpc.addons.list.useQuery()`.
6833
+ * This also guards mixed-version module-federation loads where the host
6834
+ * `@camstack/types` enum has a new member before the loaded ui-library bundle
6835
+ * has the matching `DEVICE_TYPE_META` entry.
6836
+ */
6837
+ function deviceTypeMetaOf(type) {
6838
+ if (!isDeviceType(type)) return FALLBACK_DEVICE_TYPE_META;
6839
+ return DEVICE_TYPE_META[type] ?? FALLBACK_DEVICE_TYPE_META;
6840
+ }
6841
+ /** Runtime-string-safe role lookup; unknown roles fall back to `GenericSensor`. */
6842
+ function deviceRoleMetaOf(role) {
6843
+ if (!isDeviceRole(role)) return FALLBACK_DEVICE_ROLE_META;
6844
+ return DEVICE_ROLE_META[role] ?? FALLBACK_DEVICE_ROLE_META;
6845
+ }
6846
+ /**
6847
+ * Icons an operator may pick for a per-device `display.icon` override (and for
6848
+ * a per-role default), keyed by a stable kebab-case name. Named imports only —
6849
+ * no dynamic indexing into lucide, so the bundle carries exactly this set.
6850
+ * Unknown keys resolve to `undefined` (caller falls through to the next level).
6478
6851
  *
6479
- * Push-subscription methods (`kind: 'subscription'`) emit a typeof
6480
- * alias over `useSubscription`, so the call shape is
6481
- * `useToastOnToast(input, { onData: cb })` — same as the underlying
6482
- * tRPC hook. The eventBus-backed `useLiveEvent` (see
6483
- * `../hooks/use-live-event.ts`) covers events that don't ride a tRPC
6484
- * subscription; the two surfaces coexist by design.
6852
+ * The vocabulary is the one the VIEWER draws from too (its own icon module,
6853
+ * keyed by these same strings): a key added here that the phone cannot draw
6854
+ * falls through to the next resolution level there, which reads as "my change
6855
+ * did nothing". So the KEY is the contract — a stable kebab-case string, never
6856
+ * a glyph identity — and `device-meta.test.ts` pins that every key resolves
6857
+ * and that the set never loses a key an operator may already have persisted.
6858
+ *
6859
+ * Grouped by what an operator is naming: a device, a light or appliance, a
6860
+ * sensor or the weather it reads, a room, or what moves through it. `gauge2`
6861
+ * is a legacy alias kept for rows that persisted it.
6485
6862
  */
6486
- /** Generated alias around `trpc.accessories.setChildHidden.useMutation`. */
6487
- var useAccessoriesSetChildHidden = trpc.accessories.setChildHidden.useMutation;
6488
- /** Generated alias around `trpc.accessories.getStatus.useQuery`. */
6489
- var useAccessoriesGetStatus = trpc.accessories.getStatus.useQuery;
6490
- /** Generated alias around `trpc.addonPages.listPages.useQuery`. */
6491
- var useAddonPagesListPages = trpc.addonPages.listPages.useQuery;
6492
- /** Generated alias around `trpc.addons.list.useQuery`. */
6493
- var useAddonsList = trpc.addons.list.useQuery;
6494
- /** Generated alias around `trpc.addons.getLogs.useQuery`. */
6495
- var useAddonsGetLogs = trpc.addons.getLogs.useQuery;
6496
- /** Generated alias around `trpc.addons.listPackages.useQuery`. */
6497
- var useAddonsListPackages = trpc.addons.listPackages.useQuery;
6498
- /** Generated alias around `trpc.addons.installPackage.useMutation`. */
6499
- var useAddonsInstallPackage = trpc.addons.installPackage.useMutation;
6500
- /** Generated alias around `trpc.addons.installFromWorkspace.useMutation`. */
6501
- var useAddonsInstallFromWorkspace = trpc.addons.installFromWorkspace.useMutation;
6502
- /** Generated alias around `trpc.addons.isWorkspaceAvailable.useQuery`. */
6503
- var useAddonsIsWorkspaceAvailable = trpc.addons.isWorkspaceAvailable.useQuery;
6504
- /** Generated alias around `trpc.addons.listWorkspacePackages.useQuery`. */
6505
- var useAddonsListWorkspacePackages = trpc.addons.listWorkspacePackages.useQuery;
6506
- /** Generated alias around `trpc.addons.uninstallPackage.useMutation`. */
6507
- var useAddonsUninstallPackage = trpc.addons.uninstallPackage.useMutation;
6508
- /** Generated alias around `trpc.addons.listInstallations.useQuery`. */
6509
- var useAddonsListInstallations = trpc.addons.listInstallations.useQuery;
6510
- /** Generated alias around `trpc.addons.reloadPackages.useMutation`. */
6511
- var useAddonsReloadPackages = trpc.addons.reloadPackages.useMutation;
6512
- /** Generated alias around `trpc.addons.searchAvailable.useQuery`. */
6513
- var useAddonsSearchAvailable = trpc.addons.searchAvailable.useQuery;
6514
- /** Generated alias around `trpc.addons.listUpdates.useQuery`. */
6515
- var useAddonsListUpdates = trpc.addons.listUpdates.useQuery;
6516
- /** Generated alias around `trpc.addons.updatePackage.useMutation`. */
6517
- var useAddonsUpdatePackage = trpc.addons.updatePackage.useMutation;
6518
- /** Generated alias around `trpc.addons.rollbackPackage.useMutation`. */
6519
- var useAddonsRollbackPackage = trpc.addons.rollbackPackage.useMutation;
6520
- /** Generated alias around `trpc.addons.forceRefresh.useMutation`. */
6521
- var useAddonsForceRefresh = trpc.addons.forceRefresh.useMutation;
6522
- /** Generated alias around `trpc.addons.restartServer.useMutation`. */
6523
- var useAddonsRestartServer = trpc.addons.restartServer.useMutation;
6524
- /** Generated alias around `trpc.addons.getLastRestart.useQuery`. */
6525
- var useAddonsGetLastRestart = trpc.addons.getLastRestart.useQuery;
6526
- /** Generated alias around `trpc.addons.listFrameworkPackages.useQuery`. */
6527
- var useAddonsListFrameworkPackages = trpc.addons.listFrameworkPackages.useQuery;
6528
- /** Generated alias around `trpc.addons.listCapabilityProviders.useQuery`. */
6529
- var useAddonsListCapabilityProviders = trpc.addons.listCapabilityProviders.useQuery;
6530
- /** Generated alias around `trpc.addons.setCapabilityProviderEnabled.useMutation`. */
6531
- var useAddonsSetCapabilityProviderEnabled = trpc.addons.setCapabilityProviderEnabled.useMutation;
6532
- /** Generated alias around `trpc.addons.getVersions.useQuery`. */
6533
- var useAddonsGetVersions = trpc.addons.getVersions.useQuery;
6534
- /** Generated alias around `trpc.addons.restartAddon.useMutation`. */
6535
- var useAddonsRestartAddon = trpc.addons.restartAddon.useMutation;
6536
- /** Generated alias around `trpc.addons.retryLoad.useMutation`. */
6537
- var useAddonsRetryLoad = trpc.addons.retryLoad.useMutation;
6538
- /** Generated alias around `trpc.addons.getAutoUpdateSettings.useQuery`. */
6539
- var useAddonsGetAutoUpdateSettings = trpc.addons.getAutoUpdateSettings.useQuery;
6540
- /** Generated alias around `trpc.addons.setAutoUpdateSettings.useMutation`. */
6541
- var useAddonsSetAutoUpdateSettings = trpc.addons.setAutoUpdateSettings.useMutation;
6542
- /** Generated alias around `trpc.addons.getAddonAutoUpdate.useQuery`. */
6543
- var useAddonsGetAddonAutoUpdate = trpc.addons.getAddonAutoUpdate.useQuery;
6544
- /** Generated alias around `trpc.addons.setAddonAutoUpdate.useMutation`. */
6545
- var useAddonsSetAddonAutoUpdate = trpc.addons.setAddonAutoUpdate.useMutation;
6546
- /** Generated alias around `trpc.addons.applyAutoUpdateToAll.useMutation`. */
6547
- var useAddonsApplyAutoUpdateToAll = trpc.addons.applyAutoUpdateToAll.useMutation;
6548
- /** Generated alias around `trpc.addons.custom.useMutation`. */
6549
- var useAddonsCustom = trpc.addons.custom.useMutation;
6550
- /** Generated alias around `trpc.addons.startJob.useMutation`. */
6551
- var useAddonsStartJob = trpc.addons.startJob.useMutation;
6552
- /** Generated alias around `trpc.addons.getJob.useQuery`. */
6553
- var useAddonsGetJob = trpc.addons.getJob.useQuery;
6554
- /** Generated alias around `trpc.addons.listJobs.useQuery`. */
6555
- var useAddonsListJobs = trpc.addons.listJobs.useQuery;
6556
- /** Generated alias around `trpc.addons.cancelJob.useMutation`. */
6557
- var useAddonsCancelJob = trpc.addons.cancelJob.useMutation;
6558
- /** Generated alias around `trpc.addons.onAddonLogs.useSubscription`. */
6559
- var useAddonsOnAddonLogs = trpc.addons.onAddonLogs.useSubscription;
6560
- /** Generated alias around `trpc.addonSettings.getGlobalSettings.useQuery`. */
6561
- var useAddonSettingsGetGlobalSettings = trpc.addonSettings.getGlobalSettings.useQuery;
6562
- /** Generated alias around `trpc.addonSettings.updateGlobalSettings.useMutation`. */
6563
- var useAddonSettingsUpdateGlobalSettings = trpc.addonSettings.updateGlobalSettings.useMutation;
6564
- /** Generated alias around `trpc.addonSettings.getIntegrationSettings.useQuery`. */
6565
- var useAddonSettingsGetIntegrationSettings = trpc.addonSettings.getIntegrationSettings.useQuery;
6566
- /** Generated alias around `trpc.addonSettings.getDeviceSettings.useQuery`. */
6567
- var useAddonSettingsGetDeviceSettings = trpc.addonSettings.getDeviceSettings.useQuery;
6568
- /** Generated alias around `trpc.addonSettings.updateDeviceSettings.useMutation`. */
6569
- var useAddonSettingsUpdateDeviceSettings = trpc.addonSettings.updateDeviceSettings.useMutation;
6570
- /** Generated alias around `trpc.addonWidgets.listWidgets.useQuery`. */
6571
- var useAddonWidgetsListWidgets = trpc.addonWidgets.listWidgets.useQuery;
6572
- /** Generated alias around `trpc.airQualitySensor.getStatus.useQuery`. */
6573
- var useAirQualitySensorGetStatus = trpc.airQualitySensor.getStatus.useQuery;
6574
- /** Generated alias around `trpc.alarmPanel.arm.useMutation`. */
6575
- var useAlarmPanelArm = trpc.alarmPanel.arm.useMutation;
6576
- /** Generated alias around `trpc.alarmPanel.disarm.useMutation`. */
6577
- var useAlarmPanelDisarm = trpc.alarmPanel.disarm.useMutation;
6578
- /** Generated alias around `trpc.alarmPanel.trigger.useMutation`. */
6579
- var useAlarmPanelTrigger = trpc.alarmPanel.trigger.useMutation;
6580
- /** Generated alias around `trpc.alarmPanel.getStatus.useQuery`. */
6581
- var useAlarmPanelGetStatus = trpc.alarmPanel.getStatus.useQuery;
6582
- /** Generated alias around `trpc.alerts.emit.useMutation`. */
6583
- var useAlertsEmit = trpc.alerts.emit.useMutation;
6584
- /** Generated alias around `trpc.alerts.update.useMutation`. */
6585
- var useAlertsUpdate = trpc.alerts.update.useMutation;
6586
- /** Generated alias around `trpc.alerts.list.useQuery`. */
6587
- var useAlertsList = trpc.alerts.list.useQuery;
6588
- /** Generated alias around `trpc.alerts.getUnreadCount.useQuery`. */
6589
- var useAlertsGetUnreadCount = trpc.alerts.getUnreadCount.useQuery;
6590
- /** Generated alias around `trpc.alerts.markRead.useMutation`. */
6591
- var useAlertsMarkRead = trpc.alerts.markRead.useMutation;
6592
- /** Generated alias around `trpc.alerts.markAllRead.useMutation`. */
6593
- var useAlertsMarkAllRead = trpc.alerts.markAllRead.useMutation;
6594
- /** Generated alias around `trpc.alerts.dismiss.useMutation`. */
6595
- var useAlertsDismiss = trpc.alerts.dismiss.useMutation;
6596
- /** Generated alias around `trpc.ambientLightSensor.getStatus.useQuery`. */
6597
- var useAmbientLightSensorGetStatus = trpc.ambientLightSensor.getStatus.useQuery;
6598
- /** Generated alias around `trpc.audioAnalysis.resolveDeviceSettings.useQuery`. */
6599
- var useAudioAnalysisResolveDeviceSettings = trpc.audioAnalysis.resolveDeviceSettings.useQuery;
6600
- /** Generated alias around `trpc.audioAnalysis.getDeviceSettingsContribution.useQuery`. */
6863
+ var DISPLAY_ICON_REGISTRY = {
6864
+ camera: Camera,
6865
+ network: Network,
6866
+ "toggle-left": ToggleLeft,
6867
+ "toggle-right": ToggleRight,
6868
+ "circle-dot": CircleDot,
6869
+ "refresh-cw": RefreshCw,
6870
+ box: Box,
6871
+ "scroll-text": ScrollText,
6872
+ workflow: Workflow,
6873
+ lock: Lock,
6874
+ "lock-open": LockOpen,
6875
+ blinds: Blinds,
6876
+ "air-vent": AirVent,
6877
+ "monitor-play": MonitorPlay,
6878
+ "sliders-horizontal": SlidersHorizontal,
6879
+ "map-pin": MapPin,
6880
+ bot: Bot,
6881
+ tractor: Tractor,
6882
+ image: Image,
6883
+ bone: Bone,
6884
+ "door-closed": DoorClosed,
6885
+ "door-open": DoorOpen,
6886
+ crosshair: Crosshair,
6887
+ eye: Eye,
6888
+ "eye-off": EyeOff,
6889
+ smartphone: Smartphone,
6890
+ phone: Phone,
6891
+ tablet: Tablet,
6892
+ laptop: Laptop,
6893
+ monitor: Monitor,
6894
+ tv: Tv,
6895
+ speaker: Speaker,
6896
+ printer: Printer,
6897
+ projector: Projector,
6898
+ "gamepad-2": Gamepad2,
6899
+ radio: Radio,
6900
+ router: Router,
6901
+ bluetooth: Bluetooth,
6902
+ cpu: Cpu,
6903
+ "hard-drive": HardDrive,
6904
+ server: Server,
6905
+ mic: Mic,
6906
+ "mic-off": MicOff,
6907
+ video: Video,
6908
+ "video-off": VideoOff,
6909
+ music: Music,
6910
+ plug: Plug,
6911
+ "plug-zap": PlugZap,
6912
+ power: Power,
6913
+ "power-off": PowerOff,
6914
+ cable: Cable,
6915
+ battery: Battery,
6916
+ "battery-charging": BatteryCharging,
6917
+ "battery-full": BatteryFull,
6918
+ "battery-low": BatteryLow,
6919
+ key: Key,
6920
+ "key-round": KeyRound,
6921
+ mailbox: Mailbox,
6922
+ package: Package,
6923
+ shield: Shield,
6924
+ "shield-check": ShieldCheck,
6925
+ "shield-off": ShieldOff,
6926
+ lightbulb: Lightbulb,
6927
+ lamp: Lamp,
6928
+ "lamp-ceiling": LampCeiling,
6929
+ "lamp-desk": LampDesk,
6930
+ "lamp-floor": LampFloor,
6931
+ flashlight: Flashlight,
6932
+ heater: Heater,
6933
+ snowflake: Snowflake,
6934
+ "thermometer-sun": ThermometerSun,
6935
+ "thermometer-snowflake": ThermometerSnowflake,
6936
+ refrigerator: Refrigerator,
6937
+ microwave: Microwave,
6938
+ "washing-machine": WashingMachine,
6939
+ "cooking-pot": CookingPot,
6940
+ utensils: Utensils,
6941
+ thermometer: Thermometer,
6942
+ droplets: Droplets,
6943
+ droplet: Droplet,
6944
+ gauge: Gauge,
6945
+ gauge2: Gauge,
6946
+ sun: Sun,
6947
+ "sun-medium": SunMedium,
6948
+ sunrise: Sunrise,
6949
+ moon: Moon,
6950
+ wind: Wind,
6951
+ cloud: Cloud,
6952
+ "cloud-rain": CloudRain,
6953
+ "cloud-sun": CloudSun,
6954
+ waves: Waves,
6955
+ sprout: Sprout,
6956
+ leaf: Leaf,
6957
+ flower: Flower,
6958
+ zap: Zap,
6959
+ flame: Flame,
6960
+ bell: Bell,
6961
+ "bell-ring": BellRing,
6962
+ "bell-off": BellOff,
6963
+ siren: Siren,
6964
+ radar: Radar,
6965
+ activity: Activity,
6966
+ hash: Hash,
6967
+ clock: Clock,
6968
+ timer: Timer,
6969
+ calendar: Calendar,
6970
+ pipette: Pipette,
6971
+ fan: Fan,
6972
+ bath: Bath,
6973
+ footprints: Footprints,
6974
+ "alarm-smoke": AlarmSmoke,
6975
+ "shield-alert": ShieldAlert,
6976
+ vibrate: Vibrate,
6977
+ wifi: Wifi,
6978
+ "wifi-off": WifiOff,
6979
+ "volume-2": Volume2,
6980
+ "volume-x": VolumeX,
6981
+ heart: Heart,
6982
+ "heart-pulse": HeartPulse,
6983
+ star: Star,
6984
+ sparkles: Sparkles,
6985
+ house: House,
6986
+ sofa: Sofa,
6987
+ "bed-double": BedDouble,
6988
+ bed: Bed,
6989
+ warehouse: Warehouse,
6990
+ trees: Trees,
6991
+ "tree-pine": TreePine,
6992
+ fence: Fence,
6993
+ store: Store,
6994
+ building: Building,
6995
+ "building-2": Building2,
6996
+ mountain: Mountain,
6997
+ umbrella: Umbrella,
6998
+ dumbbell: Dumbbell,
6999
+ wrench: Wrench,
7000
+ hammer: Hammer,
7001
+ drill: Drill,
7002
+ "waves-ladder": WavesLadder,
7003
+ "circle-parking": CircleParking,
7004
+ briefcase: Briefcase,
7005
+ route: Route,
7006
+ car: Car,
7007
+ bike: Bike,
7008
+ bus: Bus,
7009
+ truck: Truck,
7010
+ plane: Plane,
7011
+ "train-front": TrainFront,
7012
+ sailboat: Sailboat,
7013
+ dog: Dog,
7014
+ cat: Cat,
7015
+ bird: Bird,
7016
+ "paw-print": PawPrint,
7017
+ baby: Baby,
7018
+ user: User,
7019
+ users: Users,
7020
+ "person-standing": PersonStanding
7021
+ };
7022
+ /** Resolve a `display.icon` name to a lucide component, or `undefined` when the
7023
+ * name is not in the curated `DISPLAY_ICON_REGISTRY`. */
7024
+ function resolveDisplayIcon(name) {
7025
+ return Object.hasOwn(DISPLAY_ICON_REGISTRY, name) ? DISPLAY_ICON_REGISTRY[name] : void 0;
7026
+ }
7027
+ /**
7028
+ * EXHAUSTIVE device-type filter options — one entry per `DeviceType` member,
7029
+ * derived from `DEVICE_TYPE_META` so the set is always complete and a newly
7030
+ * added `DeviceType` automatically appears in every type filter (Devices page,
7031
+ * adoption modal, …) without touching any consumer.
7032
+ *
7033
+ * The previous per-consumer logic built options from the types PRESENT in the
7034
+ * list (or a hand-maintained subset), so recently-added types (vacuum, lawn
7035
+ * mower, valve, humidifier, water heater, weather, image, …) were silently
7036
+ * missing from the dropdown. This is the single source of truth instead.
7037
+ *
7038
+ * `id` is the raw `DeviceType` string (matches the filtering predicate, which
7039
+ * compares against `device.type`); `label` + `icon` come from the meta map.
7040
+ * Ordered by `DeviceType` declaration order for a stable, intentional layout.
7041
+ */
7042
+ var ALL_DEVICE_TYPE_FILTER_OPTIONS = Object.values(_camstack_types.DeviceType).map((type) => {
7043
+ const meta = deviceTypeMetaOf(type);
7044
+ return {
7045
+ id: type,
7046
+ label: meta.label,
7047
+ icon: meta.icon
7048
+ };
7049
+ });
7050
+ function allDeviceTypeFilterOptions() {
7051
+ return ALL_DEVICE_TYPE_FILTER_OPTIONS;
7052
+ }
7053
+ //#endregion
7054
+ //#region src/composites/icon-picker.tsx
7055
+ /**
7056
+ * icon-picker — the ONE control for choosing an icon, wherever admin-ui asks.
7057
+ *
7058
+ * There were two: a device one over `DISPLAY_ICON_REGISTRY` and a room one
7059
+ * over `LOCATION_ICON_IDS`, identical apart from the vocabulary they walked.
7060
+ * That was tolerable while the device set was 56 glyphs in a flat grid. It is
7061
+ * not at 157: a picker that only lets the operator HUNT is a picker they stop
7062
+ * using, and the two copies were already drifting — only one of them had an
7063
+ * i18n'd label.
7064
+ *
7065
+ * So the vocabulary, the grouping and the search all come from
7066
+ * `@camstack/types`' `display-icon-vocabulary`, which is the same module the
7067
+ * viewer's sheet searches. What is left here is the web half: a popover, a
7068
+ * grid, and a text field.
7069
+ *
7070
+ * ## What it does NOT own
7071
+ *
7072
+ * - **The glyphs.** `resolveGlyph` is a prop, because this package's table is
7073
+ * `lucide-react` and the viewer's is `lucide-react-native`. A key this build
7074
+ * cannot draw is SKIPPED rather than rendered as a hole — an empty tile
7075
+ * reads as a rendering bug, not as a missing choice.
7076
+ * - **The words.** Every string is a prop. This package has no i18n (one file
7077
+ * in it imports react-i18next, and that is the exception), and a control
7078
+ * that reached for a translation would force it on every consumer.
7079
+ * - **The write.** `onPick` is called and that is all; whether it is a form
7080
+ * value or an immediate mutation is the caller's business, as it already was
7081
+ * in both pickers this replaces.
7082
+ *
7083
+ * ## The clear tile is never filtered away
7084
+ *
7085
+ * The "inherit" / "no icon" choice sits above the sections and ignores the
7086
+ * search. A query that hid it would strand the operator on a glyph they picked
7087
+ * by accident with no way back to the default — the one action a picker must
7088
+ * always offer.
7089
+ */
7090
+ function IconPicker({ value, onPick, resolveGlyph, fallbackGlyph, labels, only, extraWords, clearGlyph, labelFor, disabled = false }) {
7091
+ const [open, setOpen] = (0, react$1.useState)(false);
7092
+ const [query, setQuery] = (0, react$1.useState)("");
7093
+ const allowed = (0, react$1.useMemo)(() => only === void 0 ? null : new Set(only), [only]);
7094
+ const sections = (0, react$1.useMemo)(() => {
7095
+ const hits = (0, _camstack_types.searchDisplayIcons)(query, extraWords);
7096
+ const byGroup = /* @__PURE__ */ new Map();
7097
+ for (const hit of hits) {
7098
+ const { key, group } = hit.entry;
7099
+ if (allowed !== null && !allowed.has(key)) continue;
7100
+ if (resolveGlyph(key) === void 0) continue;
7101
+ const bucket = byGroup.get(group);
7102
+ if (bucket === void 0) byGroup.set(group, [key]);
7103
+ else bucket.push(key);
7104
+ }
7105
+ return _camstack_types.DISPLAY_ICON_GROUPS.flatMap((group) => {
7106
+ const keys = byGroup.get(group);
7107
+ return keys === void 0 || keys.length === 0 ? [] : [{
7108
+ group,
7109
+ keys
7110
+ }];
7111
+ });
7112
+ }, [
7113
+ query,
7114
+ extraWords,
7115
+ allowed,
7116
+ resolveGlyph
7117
+ ]);
7118
+ const Current = (value === null ? void 0 : resolveGlyph(value)) ?? fallbackGlyph;
7119
+ const choose = async (next) => {
7120
+ setOpen(false);
7121
+ setQuery("");
7122
+ await onPick(next);
7123
+ };
7124
+ const Clear = clearGlyph ?? fallbackGlyph;
7125
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7126
+ className: "relative flex shrink-0 items-center gap-2",
7127
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7128
+ type: "button",
7129
+ disabled,
7130
+ onClick: () => setOpen((o) => !o),
7131
+ "aria-expanded": open,
7132
+ "aria-label": labels.trigger,
7133
+ title: labels.trigger,
7134
+ className: "rounded border border-border p-1 text-foreground-muted hover:bg-surface-hover disabled:opacity-50",
7135
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Current, { className: "h-4 w-4" })
7136
+ }), open && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7137
+ type: "button",
7138
+ "aria-label": labels.close,
7139
+ onClick: () => setOpen(false),
7140
+ className: "fixed inset-0 z-10 cursor-default"
7141
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7142
+ role: "group",
7143
+ "aria-label": labels.panel,
7144
+ className: "absolute left-0 top-full z-20 mt-1 max-h-80 w-72 overflow-y-auto rounded-md border border-border bg-surface p-2 shadow-lg",
7145
+ children: [
7146
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
7147
+ type: "search",
7148
+ value: query,
7149
+ autoFocus: true,
7150
+ onChange: (e) => setQuery(e.target.value),
7151
+ placeholder: labels.searchPlaceholder,
7152
+ "aria-label": labels.searchPlaceholder,
7153
+ className: "mb-2 w-full rounded border border-border bg-surface-raised px-2 py-1 text-xs text-foreground placeholder:text-foreground-subtle"
7154
+ }),
7155
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
7156
+ type: "button",
7157
+ onClick: () => void choose(null),
7158
+ "aria-pressed": value === null,
7159
+ className: "mb-2 flex w-full items-center gap-1.5 rounded px-1.5 py-1 text-[11px] text-foreground-subtle hover:bg-surface-hover",
7160
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Clear, { className: "h-3.5 w-3.5" }), labels.clear]
7161
+ }),
7162
+ sections.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7163
+ className: "px-1.5 py-2 text-[11px] text-foreground-subtle",
7164
+ children: labels.noMatches
7165
+ }) : sections.map(({ group, keys }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
7166
+ className: "mb-2 last:mb-0",
7167
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
7168
+ className: "px-1.5 pb-1 text-[10px] uppercase tracking-wide text-foreground-subtle",
7169
+ children: labels.groups[group]
7170
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7171
+ className: "grid grid-cols-7 gap-1",
7172
+ children: keys.map((key) => {
7173
+ const Glyph = resolveGlyph(key);
7174
+ if (Glyph === void 0) return null;
7175
+ const selected = value === key;
7176
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7177
+ type: "button",
7178
+ onClick: () => void choose(key),
7179
+ "aria-pressed": selected,
7180
+ "aria-label": labelFor?.(key) ?? key,
7181
+ title: labelFor?.(key) ?? key,
7182
+ className: selected ? "flex items-center justify-center rounded border border-primary bg-primary/10 p-1.5 text-foreground" : "flex items-center justify-center rounded border border-transparent p-1.5 text-foreground-muted hover:bg-surface-hover",
7183
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Glyph, { className: "h-4 w-4" })
7184
+ }, key);
7185
+ })
7186
+ })]
7187
+ }, group))
7188
+ ]
7189
+ })] })]
7190
+ });
7191
+ }
7192
+ //#endregion
7193
+ //#region src/composites/config-icon-field.tsx
7194
+ /**
7195
+ * The control behind a `type: 'icon'` settings field (D780).
7196
+ *
7197
+ * The shared `<IconPicker>` over the display-icon vocabulary, with the device
7198
+ * glyph table — the same control and the same keys the device and room
7199
+ * pickers use. An icon field always draws something, so clearing is "back to
7200
+ * the field's default", and a stored value the vocabulary does not know (a
7201
+ * free-text lucide name from before D780) is drawn as that default.
7202
+ */
7203
+ function labelsFor(label) {
7204
+ return {
7205
+ trigger: `Choose an icon for ${label}`,
7206
+ panel: `${label} icon`,
7207
+ close: "Close",
7208
+ searchPlaceholder: "Search icons",
7209
+ clear: "Default icon",
7210
+ noMatches: "No icon matches",
7211
+ groups: {
7212
+ device: "Devices",
7213
+ lighting: "Lighting",
7214
+ sensing: "Sensing",
7215
+ place: "Places",
7216
+ life: "Life"
7217
+ }
7218
+ };
7219
+ }
7220
+ function ConfigIconFieldControl({ label, value, defaultKey, disabled, onChange }) {
7221
+ const key = (0, _camstack_types.resolveDisplayIconKey)(value, defaultKey);
7222
+ const DefaultGlyph = resolveDisplayIcon(defaultKey) ?? ShieldCheck;
7223
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7224
+ className: "flex items-center gap-2",
7225
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconPicker, {
7226
+ value: key === "" ? null : key,
7227
+ onPick: (next) => onChange(next ?? defaultKey),
7228
+ resolveGlyph: resolveDisplayIcon,
7229
+ fallbackGlyph: DefaultGlyph,
7230
+ labels: labelsFor(label),
7231
+ disabled
7232
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7233
+ className: "text-[11px] text-foreground-subtle font-mono",
7234
+ children: key
7235
+ })]
7236
+ });
7237
+ }
7238
+ //#endregion
7239
+ //#region src/composites/config-secret-field.tsx
7240
+ /**
7241
+ * A keychain-stored secret in a settings form (D787, D789).
7242
+ *
7243
+ * The form never holds the value. It receives the field's VIEW —
7244
+ * `{ $secret, v, set: true }` when something is stored, `null` when nothing
7245
+ * is — and sends back exactly one of three things:
7246
+ *
7247
+ * - the view, unchanged ........ KEEP (also what an untouched form saves);
7248
+ * - a non-empty typed string ... CHANGE (the owner stores it in the keychain);
7249
+ * - `{ $secretClear: true }` ... CLEAR (the only thing that removes it).
7250
+ *
7251
+ * `''` is never sent for "remove": the save path reads it as KEEP, because an
7252
+ * empty string is what a defaults fallback writes back.
7253
+ *
7254
+ * Reveal shows the stored value for a few seconds, through the host's audited
7255
+ * `keychain-admin.reveal` (provided with {@link SecretRevealProvider}); a host
7256
+ * that provides none has no Reveal button.
7257
+ */
7258
+ /** How long a revealed value stays on screen. */
7259
+ var SECRET_REVEAL_MS = 1e4;
7260
+ var SecretRevealContext = createSharedContext("camstack:secret-reveal", null);
7261
+ function SecretRevealProvider({ reveal, children }) {
7262
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SecretRevealContext.Provider, {
7263
+ value: reveal,
7264
+ children
7265
+ });
7266
+ }
7267
+ /** Does this value belong to the keychain rendering (a view, or "not set")? */
7268
+ function isKeychainFieldValue(value) {
7269
+ return value === null || (0, _camstack_types.isSecretFieldView)(value) || (0, _camstack_types.isSecretClear)(value);
7270
+ }
7271
+ var BUTTON$1 = "inline-flex items-center gap-1 rounded border border-border bg-surface px-2 py-0.5 text-[11px] font-medium text-foreground hover:border-primary/40 hover:bg-primary/5 disabled:opacity-40 disabled:cursor-not-allowed";
7272
+ var INPUT = "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";
7273
+ function SecretFieldControl({ field, value, onChange, disabled, multiline }) {
7274
+ const reveal = (0, react$1.useContext)(SecretRevealContext);
7275
+ const [original] = (0, react$1.useState)(() => (0, _camstack_types.isSecretFieldView)(value) ? value : null);
7276
+ const [mode, setMode] = (0, react$1.useState)(() => typeof value === "string" && value.length > 0 ? {
7277
+ kind: "editing",
7278
+ text: value
7279
+ } : (0, _camstack_types.isSecretClear)(value) ? { kind: "clearing" } : { kind: "idle" });
7280
+ const [shown, setShown] = (0, react$1.useState)(null);
7281
+ const [revealError, setRevealError] = (0, react$1.useState)(null);
7282
+ const [showTyped, setShowTyped] = (0, react$1.useState)(false);
7283
+ (0, react$1.useEffect)(() => {
7284
+ if (shown === null) return;
7285
+ const timer = setTimeout(() => setShown(null), SECRET_REVEAL_MS);
7286
+ return () => clearTimeout(timer);
7287
+ }, [shown]);
7288
+ const isSet = original !== null;
7289
+ const ref = original?.$secret ?? null;
7290
+ const keep = () => {
7291
+ setMode({ kind: "idle" });
7292
+ onChange(original);
7293
+ };
7294
+ const label = "label" in field ? field.label : "";
7295
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7296
+ className: "space-y-1",
7297
+ "data-secret-field": "key" in field ? field.key : void 0,
7298
+ children: [
7299
+ mode.kind === "editing" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7300
+ className: "flex items-start gap-1.5",
7301
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7302
+ className: "relative flex-1",
7303
+ children: [multiline ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
7304
+ className: `${INPUT} min-h-[60px] font-mono`,
7305
+ "aria-label": label,
7306
+ value: mode.text,
7307
+ autoComplete: "off",
7308
+ spellCheck: false,
7309
+ disabled,
7310
+ onChange: (e) => {
7311
+ setMode({
7312
+ kind: "editing",
7313
+ text: e.target.value
7314
+ });
7315
+ onChange(e.target.value.length > 0 ? e.target.value : original);
7316
+ }
7317
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
7318
+ type: showTyped ? "text" : "password",
7319
+ className: `${INPUT} pr-8`,
7320
+ "aria-label": label,
7321
+ value: mode.text,
7322
+ autoComplete: "new-password",
7323
+ spellCheck: false,
7324
+ disabled,
7325
+ autoFocus: true,
7326
+ onChange: (e) => {
7327
+ setMode({
7328
+ kind: "editing",
7329
+ text: e.target.value
7330
+ });
7331
+ onChange(e.target.value.length > 0 ? e.target.value : original);
7332
+ }
7333
+ }), !multiline && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7334
+ type: "button",
7335
+ className: "absolute right-2 top-1/2 -translate-y-1/2 text-foreground-subtle hover:text-foreground",
7336
+ tabIndex: -1,
7337
+ "aria-label": showTyped ? "Hide" : "Show",
7338
+ onClick: () => setShowTyped((s) => !s),
7339
+ children: showTyped ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EyeOff, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Eye, { className: "h-3.5 w-3.5" })
7340
+ })]
7341
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7342
+ type: "button",
7343
+ className: BUTTON$1,
7344
+ onClick: keep,
7345
+ disabled,
7346
+ children: "Cancel"
7347
+ })]
7348
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7349
+ className: "flex flex-wrap items-center gap-1.5",
7350
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
7351
+ className: `inline-flex items-center gap-1 rounded-md border px-2 py-1 text-xs ${mode.kind === "clearing" ? "border-danger/40 text-danger" : isSet ? "border-border text-foreground" : "border-dashed border-border text-foreground-subtle"}`,
7352
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(KeyRound, { className: "h-3 w-3" }), mode.kind === "clearing" ? "will be removed on save" : shown !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
7353
+ className: "font-mono break-all",
7354
+ children: shown
7355
+ }) : isSet ? "●●●●●● set" : "not set"]
7356
+ }), mode.kind === "clearing" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7357
+ type: "button",
7358
+ className: BUTTON$1,
7359
+ onClick: keep,
7360
+ disabled,
7361
+ children: "Undo"
7362
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
7363
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7364
+ type: "button",
7365
+ className: BUTTON$1,
7366
+ disabled,
7367
+ onClick: () => setMode({
7368
+ kind: "editing",
7369
+ text: ""
7370
+ }),
7371
+ children: isSet ? "Change" : "Set"
7372
+ }),
7373
+ isSet && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7374
+ type: "button",
7375
+ className: BUTTON$1,
7376
+ disabled,
7377
+ onClick: () => {
7378
+ setShown(null);
7379
+ setMode({ kind: "clearing" });
7380
+ onChange(_camstack_types.SECRET_CLEAR);
7381
+ },
7382
+ children: "Clear"
7383
+ }),
7384
+ isSet && ref !== null && reveal !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7385
+ type: "button",
7386
+ className: BUTTON$1,
7387
+ disabled,
7388
+ onClick: () => {
7389
+ if (shown !== null) {
7390
+ setShown(null);
7391
+ return;
7392
+ }
7393
+ setRevealError(null);
7394
+ reveal(ref).then(setShown, (err) => setRevealError(err instanceof Error ? err.message : String(err)));
7395
+ },
7396
+ children: shown !== null ? "Hide" : "Reveal"
7397
+ })
7398
+ ] })]
7399
+ }),
7400
+ original?.inline === true && mode.kind === "idle" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7401
+ className: "text-[10px] text-foreground-subtle",
7402
+ children: "Stored before the keychain; it moves there the next time it is changed."
7403
+ }),
7404
+ revealError !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7405
+ className: "text-[10px] text-danger",
7406
+ children: revealError
7407
+ })
7408
+ ]
7409
+ });
7410
+ }
7411
+ //#endregion
7412
+ //#region src/generated/system-hooks.ts
7413
+ /**
7414
+ * Per-method React-Query hooks for every `scope: 'system'` capability.
7415
+ *
7416
+ * Each hook is a thin pass-through to `trpc.<cap>.<method>.useQuery` /
7417
+ * `.useMutation` / `.useSubscription` so the typed inference + RQ
7418
+ * cache/dedup keep working — the generator only contributes a consistent
7419
+ * name and stable export surface so admin-ui pages can write
7420
+ * `useAddonsList()` instead of `trpc.addons.list.useQuery()`.
7421
+ *
7422
+ * Push-subscription methods (`kind: 'subscription'`) emit a typeof
7423
+ * alias over `useSubscription`, so the call shape is
7424
+ * `useToastOnToast(input, { onData: cb })` — same as the underlying
7425
+ * tRPC hook. The eventBus-backed `useLiveEvent` (see
7426
+ * `../hooks/use-live-event.ts`) covers events that don't ride a tRPC
7427
+ * subscription; the two surfaces coexist by design.
7428
+ */
7429
+ /** Generated alias around `trpc.accessories.setChildHidden.useMutation`. */
7430
+ var useAccessoriesSetChildHidden = trpc.accessories.setChildHidden.useMutation;
7431
+ /** Generated alias around `trpc.accessories.getStatus.useQuery`. */
7432
+ var useAccessoriesGetStatus = trpc.accessories.getStatus.useQuery;
7433
+ /** Generated alias around `trpc.addonPages.listPages.useQuery`. */
7434
+ var useAddonPagesListPages = trpc.addonPages.listPages.useQuery;
7435
+ /** Generated alias around `trpc.addons.list.useQuery`. */
7436
+ var useAddonsList = trpc.addons.list.useQuery;
7437
+ /** Generated alias around `trpc.addons.getLogs.useQuery`. */
7438
+ var useAddonsGetLogs = trpc.addons.getLogs.useQuery;
7439
+ /** Generated alias around `trpc.addons.listPackages.useQuery`. */
7440
+ var useAddonsListPackages = trpc.addons.listPackages.useQuery;
7441
+ /** Generated alias around `trpc.addons.installPackage.useMutation`. */
7442
+ var useAddonsInstallPackage = trpc.addons.installPackage.useMutation;
7443
+ /** Generated alias around `trpc.addons.installFromWorkspace.useMutation`. */
7444
+ var useAddonsInstallFromWorkspace = trpc.addons.installFromWorkspace.useMutation;
7445
+ /** Generated alias around `trpc.addons.isWorkspaceAvailable.useQuery`. */
7446
+ var useAddonsIsWorkspaceAvailable = trpc.addons.isWorkspaceAvailable.useQuery;
7447
+ /** Generated alias around `trpc.addons.listWorkspacePackages.useQuery`. */
7448
+ var useAddonsListWorkspacePackages = trpc.addons.listWorkspacePackages.useQuery;
7449
+ /** Generated alias around `trpc.addons.uninstallPackage.useMutation`. */
7450
+ var useAddonsUninstallPackage = trpc.addons.uninstallPackage.useMutation;
7451
+ /** Generated alias around `trpc.addons.listInstallations.useQuery`. */
7452
+ var useAddonsListInstallations = trpc.addons.listInstallations.useQuery;
7453
+ /** Generated alias around `trpc.addons.reloadPackages.useMutation`. */
7454
+ var useAddonsReloadPackages = trpc.addons.reloadPackages.useMutation;
7455
+ /** Generated alias around `trpc.addons.searchAvailable.useQuery`. */
7456
+ var useAddonsSearchAvailable = trpc.addons.searchAvailable.useQuery;
7457
+ /** Generated alias around `trpc.addons.listUpdates.useQuery`. */
7458
+ var useAddonsListUpdates = trpc.addons.listUpdates.useQuery;
7459
+ /** Generated alias around `trpc.addons.updatePackage.useMutation`. */
7460
+ var useAddonsUpdatePackage = trpc.addons.updatePackage.useMutation;
7461
+ /** Generated alias around `trpc.addons.rollbackPackage.useMutation`. */
7462
+ var useAddonsRollbackPackage = trpc.addons.rollbackPackage.useMutation;
7463
+ /** Generated alias around `trpc.addons.forceRefresh.useMutation`. */
7464
+ var useAddonsForceRefresh = trpc.addons.forceRefresh.useMutation;
7465
+ /** Generated alias around `trpc.addons.restartServer.useMutation`. */
7466
+ var useAddonsRestartServer = trpc.addons.restartServer.useMutation;
7467
+ /** Generated alias around `trpc.addons.getLastRestart.useQuery`. */
7468
+ var useAddonsGetLastRestart = trpc.addons.getLastRestart.useQuery;
7469
+ /** Generated alias around `trpc.addons.listFrameworkPackages.useQuery`. */
7470
+ var useAddonsListFrameworkPackages = trpc.addons.listFrameworkPackages.useQuery;
7471
+ /** Generated alias around `trpc.addons.listCapabilityProviders.useQuery`. */
7472
+ var useAddonsListCapabilityProviders = trpc.addons.listCapabilityProviders.useQuery;
7473
+ /** Generated alias around `trpc.addons.setCapabilityProviderEnabled.useMutation`. */
7474
+ var useAddonsSetCapabilityProviderEnabled = trpc.addons.setCapabilityProviderEnabled.useMutation;
7475
+ /** Generated alias around `trpc.addons.getVersions.useQuery`. */
7476
+ var useAddonsGetVersions = trpc.addons.getVersions.useQuery;
7477
+ /** Generated alias around `trpc.addons.restartAddon.useMutation`. */
7478
+ var useAddonsRestartAddon = trpc.addons.restartAddon.useMutation;
7479
+ /** Generated alias around `trpc.addons.retryLoad.useMutation`. */
7480
+ var useAddonsRetryLoad = trpc.addons.retryLoad.useMutation;
7481
+ /** Generated alias around `trpc.addons.getAutoUpdateSettings.useQuery`. */
7482
+ var useAddonsGetAutoUpdateSettings = trpc.addons.getAutoUpdateSettings.useQuery;
7483
+ /** Generated alias around `trpc.addons.setAutoUpdateSettings.useMutation`. */
7484
+ var useAddonsSetAutoUpdateSettings = trpc.addons.setAutoUpdateSettings.useMutation;
7485
+ /** Generated alias around `trpc.addons.getAddonAutoUpdate.useQuery`. */
7486
+ var useAddonsGetAddonAutoUpdate = trpc.addons.getAddonAutoUpdate.useQuery;
7487
+ /** Generated alias around `trpc.addons.setAddonAutoUpdate.useMutation`. */
7488
+ var useAddonsSetAddonAutoUpdate = trpc.addons.setAddonAutoUpdate.useMutation;
7489
+ /** Generated alias around `trpc.addons.applyAutoUpdateToAll.useMutation`. */
7490
+ var useAddonsApplyAutoUpdateToAll = trpc.addons.applyAutoUpdateToAll.useMutation;
7491
+ /** Generated alias around `trpc.addons.custom.useMutation`. */
7492
+ var useAddonsCustom = trpc.addons.custom.useMutation;
7493
+ /** Generated alias around `trpc.addons.startJob.useMutation`. */
7494
+ var useAddonsStartJob = trpc.addons.startJob.useMutation;
7495
+ /** Generated alias around `trpc.addons.getJob.useQuery`. */
7496
+ var useAddonsGetJob = trpc.addons.getJob.useQuery;
7497
+ /** Generated alias around `trpc.addons.listJobs.useQuery`. */
7498
+ var useAddonsListJobs = trpc.addons.listJobs.useQuery;
7499
+ /** Generated alias around `trpc.addons.cancelJob.useMutation`. */
7500
+ var useAddonsCancelJob = trpc.addons.cancelJob.useMutation;
7501
+ /** Generated alias around `trpc.addons.onAddonLogs.useSubscription`. */
7502
+ var useAddonsOnAddonLogs = trpc.addons.onAddonLogs.useSubscription;
7503
+ /** Generated alias around `trpc.addonSettings.getGlobalSettings.useQuery`. */
7504
+ var useAddonSettingsGetGlobalSettings = trpc.addonSettings.getGlobalSettings.useQuery;
7505
+ /** Generated alias around `trpc.addonSettings.updateGlobalSettings.useMutation`. */
7506
+ var useAddonSettingsUpdateGlobalSettings = trpc.addonSettings.updateGlobalSettings.useMutation;
7507
+ /** Generated alias around `trpc.addonSettings.getIntegrationSettings.useQuery`. */
7508
+ var useAddonSettingsGetIntegrationSettings = trpc.addonSettings.getIntegrationSettings.useQuery;
7509
+ /** Generated alias around `trpc.addonSettings.getDeviceSettings.useQuery`. */
7510
+ var useAddonSettingsGetDeviceSettings = trpc.addonSettings.getDeviceSettings.useQuery;
7511
+ /** Generated alias around `trpc.addonSettings.updateDeviceSettings.useMutation`. */
7512
+ var useAddonSettingsUpdateDeviceSettings = trpc.addonSettings.updateDeviceSettings.useMutation;
7513
+ /** Generated alias around `trpc.addonWidgets.listWidgets.useQuery`. */
7514
+ var useAddonWidgetsListWidgets = trpc.addonWidgets.listWidgets.useQuery;
7515
+ /** Generated alias around `trpc.airQualitySensor.getStatus.useQuery`. */
7516
+ var useAirQualitySensorGetStatus = trpc.airQualitySensor.getStatus.useQuery;
7517
+ /** Generated alias around `trpc.alarmPanel.arm.useMutation`. */
7518
+ var useAlarmPanelArm = trpc.alarmPanel.arm.useMutation;
7519
+ /** Generated alias around `trpc.alarmPanel.disarm.useMutation`. */
7520
+ var useAlarmPanelDisarm = trpc.alarmPanel.disarm.useMutation;
7521
+ /** Generated alias around `trpc.alarmPanel.trigger.useMutation`. */
7522
+ var useAlarmPanelTrigger = trpc.alarmPanel.trigger.useMutation;
7523
+ /** Generated alias around `trpc.alarmPanel.getStatus.useQuery`. */
7524
+ var useAlarmPanelGetStatus = trpc.alarmPanel.getStatus.useQuery;
7525
+ /** Generated alias around `trpc.alerts.emit.useMutation`. */
7526
+ var useAlertsEmit = trpc.alerts.emit.useMutation;
7527
+ /** Generated alias around `trpc.alerts.update.useMutation`. */
7528
+ var useAlertsUpdate = trpc.alerts.update.useMutation;
7529
+ /** Generated alias around `trpc.alerts.list.useQuery`. */
7530
+ var useAlertsList = trpc.alerts.list.useQuery;
7531
+ /** Generated alias around `trpc.alerts.getUnreadCount.useQuery`. */
7532
+ var useAlertsGetUnreadCount = trpc.alerts.getUnreadCount.useQuery;
7533
+ /** Generated alias around `trpc.alerts.markRead.useMutation`. */
7534
+ var useAlertsMarkRead = trpc.alerts.markRead.useMutation;
7535
+ /** Generated alias around `trpc.alerts.markAllRead.useMutation`. */
7536
+ var useAlertsMarkAllRead = trpc.alerts.markAllRead.useMutation;
7537
+ /** Generated alias around `trpc.alerts.dismiss.useMutation`. */
7538
+ var useAlertsDismiss = trpc.alerts.dismiss.useMutation;
7539
+ /** Generated alias around `trpc.ambientLightSensor.getStatus.useQuery`. */
7540
+ var useAmbientLightSensorGetStatus = trpc.ambientLightSensor.getStatus.useQuery;
7541
+ /** Generated alias around `trpc.audioAnalysis.resolveDeviceSettings.useQuery`. */
7542
+ var useAudioAnalysisResolveDeviceSettings = trpc.audioAnalysis.resolveDeviceSettings.useQuery;
7543
+ /** Generated alias around `trpc.audioAnalysis.getDeviceSettingsContribution.useQuery`. */
6601
7544
  var useAudioAnalysisGetDeviceSettingsContribution = trpc.audioAnalysis.getDeviceSettingsContribution.useQuery;
6602
7545
  /** Generated alias around `trpc.audioAnalysis.getDeviceLiveContribution.useQuery`. */
6603
7546
  var useAudioAnalysisGetDeviceLiveContribution = trpc.audioAnalysis.getDeviceLiveContribution.useQuery;
@@ -7267,6 +8210,24 @@ var useIntercomPushTalkAudio = trpc.intercom.pushTalkAudio.useMutation;
7267
8210
  var useIntercomEndTalkSession = trpc.intercom.endTalkSession.useMutation;
7268
8211
  /** Generated alias around `trpc.intercom.getStatus.useQuery`. */
7269
8212
  var useIntercomGetStatus = trpc.intercom.getStatus.useQuery;
8213
+ /** Generated alias around `trpc.keychainAdmin.status.useQuery`. */
8214
+ var useKeychainAdminStatus = trpc.keychainAdmin.status.useQuery;
8215
+ /** Generated alias around `trpc.keychainAdmin.listSecrets.useQuery`. */
8216
+ var useKeychainAdminListSecrets = trpc.keychainAdmin.listSecrets.useQuery;
8217
+ /** Generated alias around `trpc.keychainAdmin.listAudit.useQuery`. */
8218
+ var useKeychainAdminListAudit = trpc.keychainAdmin.listAudit.useQuery;
8219
+ /** Generated alias around `trpc.keychainAdmin.reveal.useMutation`. */
8220
+ var useKeychainAdminReveal = trpc.keychainAdmin.reveal.useMutation;
8221
+ /** Generated alias around `trpc.keychainAdmin.deleteOrphans.useMutation`. */
8222
+ var useKeychainAdminDeleteOrphans = trpc.keychainAdmin.deleteOrphans.useMutation;
8223
+ /** Generated alias around `trpc.keychainAdmin.rotateMasterKey.useMutation`. */
8224
+ var useKeychainAdminRotateMasterKey = trpc.keychainAdmin.rotateMasterKey.useMutation;
8225
+ /** Generated alias around `trpc.keychainAdmin.purgeRetiredKeys.useMutation`. */
8226
+ var useKeychainAdminPurgeRetiredKeys = trpc.keychainAdmin.purgeRetiredKeys.useMutation;
8227
+ /** Generated alias around `trpc.keychainAdmin.downloadRecoveryKey.useMutation`. */
8228
+ var useKeychainAdminDownloadRecoveryKey = trpc.keychainAdmin.downloadRecoveryKey.useMutation;
8229
+ /** Generated alias around `trpc.keychainAdmin.resetKeychain.useMutation`. */
8230
+ var useKeychainAdminResetKeychain = trpc.keychainAdmin.resetKeychain.useMutation;
7270
8231
  /** Generated alias around `trpc.lawnMowerControl.startMowing.useMutation`. */
7271
8232
  var useLawnMowerControlStartMowing = trpc.lawnMowerControl.startMowing.useMutation;
7272
8233
  /** Generated alias around `trpc.lawnMowerControl.pause.useMutation`. */
@@ -7803,6 +8764,30 @@ var usePipelineAnalyticsCompleteRetrainTrack = trpc.pipelineAnalytics.completeRe
7803
8764
  var usePipelineAnalyticsRestageRetrainTrack = trpc.pipelineAnalytics.restageRetrainTrack.useMutation;
7804
8765
  /** Generated alias around `trpc.pipelineAnalytics.getRetrainExportUrl.useQuery`. */
7805
8766
  var usePipelineAnalyticsGetRetrainExportUrl = trpc.pipelineAnalytics.getRetrainExportUrl.useQuery;
8767
+ /** Generated alias around `trpc.pipelineAnalytics.setRetrainFrameComplete.useMutation`. */
8768
+ var usePipelineAnalyticsSetRetrainFrameComplete = trpc.pipelineAnalytics.setRetrainFrameComplete.useMutation;
8769
+ /** Generated alias around `trpc.pipelineAnalytics.writeRetrainArchive.useMutation`. */
8770
+ var usePipelineAnalyticsWriteRetrainArchive = trpc.pipelineAnalytics.writeRetrainArchive.useMutation;
8771
+ /** Generated alias around `trpc.pipelineAnalytics.listRetrainAssistProfiles.useQuery`. */
8772
+ var usePipelineAnalyticsListRetrainAssistProfiles = trpc.pipelineAnalytics.listRetrainAssistProfiles.useQuery;
8773
+ /** Generated alias around `trpc.pipelineAnalytics.proposeRetrainAnnotationsWithLlm.useMutation`. */
8774
+ var usePipelineAnalyticsProposeRetrainAnnotationsWithLlm = trpc.pipelineAnalytics.proposeRetrainAnnotationsWithLlm.useMutation;
8775
+ /** Generated alias around `trpc.pipelineAnalytics.createRetrainCollection.useMutation`. */
8776
+ var usePipelineAnalyticsCreateRetrainCollection = trpc.pipelineAnalytics.createRetrainCollection.useMutation;
8777
+ /** Generated alias around `trpc.pipelineAnalytics.listRetrainCollections.useQuery`. */
8778
+ var usePipelineAnalyticsListRetrainCollections = trpc.pipelineAnalytics.listRetrainCollections.useQuery;
8779
+ /** Generated alias around `trpc.pipelineAnalytics.renameRetrainCollection.useMutation`. */
8780
+ var usePipelineAnalyticsRenameRetrainCollection = trpc.pipelineAnalytics.renameRetrainCollection.useMutation;
8781
+ /** Generated alias around `trpc.pipelineAnalytics.deleteRetrainCollection.useMutation`. */
8782
+ var usePipelineAnalyticsDeleteRetrainCollection = trpc.pipelineAnalytics.deleteRetrainCollection.useMutation;
8783
+ /** Generated alias around `trpc.pipelineAnalytics.addRetrainFrameFromRecording.useMutation`. */
8784
+ var usePipelineAnalyticsAddRetrainFrameFromRecording = trpc.pipelineAnalytics.addRetrainFrameFromRecording.useMutation;
8785
+ /** Generated alias around `trpc.pipelineAnalytics.addRetrainFramesToCollection.useMutation`. */
8786
+ var usePipelineAnalyticsAddRetrainFramesToCollection = trpc.pipelineAnalytics.addRetrainFramesToCollection.useMutation;
8787
+ /** Generated alias around `trpc.pipelineAnalytics.removeRetrainFrameFromCollection.useMutation`. */
8788
+ var usePipelineAnalyticsRemoveRetrainFrameFromCollection = trpc.pipelineAnalytics.removeRetrainFrameFromCollection.useMutation;
8789
+ /** Generated alias around `trpc.pipelineAnalytics.listRetrainCollectionFrames.useQuery`. */
8790
+ var usePipelineAnalyticsListRetrainCollectionFrames = trpc.pipelineAnalytics.listRetrainCollectionFrames.useQuery;
7806
8791
  /** Generated alias around `trpc.pipelineAnalytics.getEventMedia.useQuery`. */
7807
8792
  var usePipelineAnalyticsGetEventMedia = trpc.pipelineAnalytics.getEventMedia.useQuery;
7808
8793
  /** Generated alias around `trpc.pipelineAnalytics.getTrackMedia.useQuery`. */
@@ -7975,6 +8960,20 @@ var usePipelineOrchestratorSuggestShares = trpc.pipelineOrchestrator.suggestShar
7975
8960
  var usePipelineOrchestratorGetEffectiveShares = trpc.pipelineOrchestrator.getEffectiveShares.useQuery;
7976
8961
  /** Generated alias around `trpc.pipelineOrchestrator.setShareSource.useMutation`. */
7977
8962
  var usePipelineOrchestratorSetShareSource = trpc.pipelineOrchestrator.setShareSource.useMutation;
8963
+ /** Generated alias around `trpc.pipelineOrchestrator.getNodeBenchmarkPreflight.useQuery`. */
8964
+ var usePipelineOrchestratorGetNodeBenchmarkPreflight = trpc.pipelineOrchestrator.getNodeBenchmarkPreflight.useQuery;
8965
+ /** Generated alias around `trpc.pipelineOrchestrator.startNodeBenchmark.useMutation`. */
8966
+ var usePipelineOrchestratorStartNodeBenchmark = trpc.pipelineOrchestrator.startNodeBenchmark.useMutation;
8967
+ /** Generated alias around `trpc.pipelineOrchestrator.getNodeBenchmarkStatus.useQuery`. */
8968
+ var usePipelineOrchestratorGetNodeBenchmarkStatus = trpc.pipelineOrchestrator.getNodeBenchmarkStatus.useQuery;
8969
+ /** Generated alias around `trpc.pipelineOrchestrator.abortNodeBenchmark.useMutation`. */
8970
+ var usePipelineOrchestratorAbortNodeBenchmark = trpc.pipelineOrchestrator.abortNodeBenchmark.useMutation;
8971
+ /** Generated alias around `trpc.pipelineOrchestrator.getNodeBenchmarkCandidates.useQuery`. */
8972
+ var usePipelineOrchestratorGetNodeBenchmarkCandidates = trpc.pipelineOrchestrator.getNodeBenchmarkCandidates.useQuery;
8973
+ /** Generated alias around `trpc.pipelineOrchestrator.listNodeBenchmarkResults.useQuery`. */
8974
+ var usePipelineOrchestratorListNodeBenchmarkResults = trpc.pipelineOrchestrator.listNodeBenchmarkResults.useQuery;
8975
+ /** Generated alias around `trpc.pipelineOrchestrator.deleteNodeBenchmarkResult.useMutation`. */
8976
+ var usePipelineOrchestratorDeleteNodeBenchmarkResult = trpc.pipelineOrchestrator.deleteNodeBenchmarkResult.useMutation;
7978
8977
  /** Generated alias around `trpc.pipelineOrchestrator.getCameraSettings.useQuery`. */
7979
8978
  var usePipelineOrchestratorGetCameraSettings = trpc.pipelineOrchestrator.getCameraSettings.useQuery;
7980
8979
  /** Generated alias around `trpc.pipelineOrchestrator.setCameraStepToggle.useMutation`. */
@@ -8039,6 +9038,12 @@ var usePipelineRunnerParkTrackFrame = trpc.pipelineRunner.parkTrackFrame.useMuta
8039
9038
  var usePipelineRunnerGetParkedTrackFrame = trpc.pipelineRunner.getParkedTrackFrame.useQuery;
8040
9039
  /** Generated alias around `trpc.pipelineRunner.releaseParkedTrackFrames.useMutation`. */
8041
9040
  var usePipelineRunnerReleaseParkedTrackFrames = trpc.pipelineRunner.releaseParkedTrackFrames.useMutation;
9041
+ /** Generated alias around `trpc.pipelineRunner.startNodeBench.useMutation`. */
9042
+ var usePipelineRunnerStartNodeBench = trpc.pipelineRunner.startNodeBench.useMutation;
9043
+ /** Generated alias around `trpc.pipelineRunner.getNodeBenchStatus.useQuery`. */
9044
+ var usePipelineRunnerGetNodeBenchStatus = trpc.pipelineRunner.getNodeBenchStatus.useQuery;
9045
+ /** Generated alias around `trpc.pipelineRunner.abortNodeBench.useMutation`. */
9046
+ var usePipelineRunnerAbortNodeBench = trpc.pipelineRunner.abortNodeBench.useMutation;
8042
9047
  /** Generated alias around `trpc.plateGallery.listPlates.useQuery`. */
8043
9048
  var usePlateGalleryListPlates = trpc.plateGallery.listPlates.useQuery;
8044
9049
  /** Generated alias around `trpc.plateGallery.getPlateByTrack.useQuery`. */
@@ -8591,6 +9596,14 @@ var useUserManagementOauthExchangeCode = trpc.userManagement.oauthExchangeCode.u
8591
9596
  var useUserManagementOauthRefresh = trpc.userManagement.oauthRefresh.useMutation;
8592
9597
  /** Generated alias around `trpc.userManagement.oauthVerifyAccessToken.useQuery`. */
8593
9598
  var useUserManagementOauthVerifyAccessToken = trpc.userManagement.oauthVerifyAccessToken.useQuery;
9599
+ /** Generated alias around `trpc.userManagement.listExternalIdentities.useQuery`. */
9600
+ var useUserManagementListExternalIdentities = trpc.userManagement.listExternalIdentities.useQuery;
9601
+ /** Generated alias around `trpc.userManagement.findUserByExternalIdentity.useQuery`. */
9602
+ var useUserManagementFindUserByExternalIdentity = trpc.userManagement.findUserByExternalIdentity.useQuery;
9603
+ /** Generated alias around `trpc.userManagement.linkExternalIdentity.useMutation`. */
9604
+ var useUserManagementLinkExternalIdentity = trpc.userManagement.linkExternalIdentity.useMutation;
9605
+ /** Generated alias around `trpc.userManagement.unlinkExternalIdentity.useMutation`. */
9606
+ var useUserManagementUnlinkExternalIdentity = trpc.userManagement.unlinkExternalIdentity.useMutation;
8594
9607
  /** Generated alias around `trpc.userManagement.listOauthSessions.useQuery`. */
8595
9608
  var useUserManagementListOauthSessions = trpc.userManagement.listOauthSessions.useQuery;
8596
9609
  /** Generated alias around `trpc.userManagement.revokeOauthSession.useMutation`. */
@@ -20876,7 +21889,7 @@ function formatDurationMs(ms) {
20876
21889
  * firmware inventing a refusal reaches the operator rather than becoming a
20877
21890
  * blank tile.
20878
21891
  */
20879
- var REFUSAL_TEXT = {
21892
+ var REFUSAL_TEXT$1 = {
20880
21893
  sleeping: () => "The camera is asleep — a still would cost a wake",
20881
21894
  "camera-refused": (reason) => `The camera refused — ${reason}`,
20882
21895
  "no-keyframe": () => "No decodable frame inside this window",
@@ -20888,7 +21901,7 @@ var REFUSAL_TEXT = {
20888
21901
  "uid-missing": () => "The camera’s id is not known yet"
20889
21902
  };
20890
21903
  function wordsFor(code, reason) {
20891
- const known = code === void 0 ? void 0 : REFUSAL_TEXT[code];
21904
+ const known = code === void 0 ? void 0 : REFUSAL_TEXT$1[code];
20892
21905
  return known === void 0 ? reason : known(reason);
20893
21906
  }
20894
21907
  function refusal(reason, code) {
@@ -24471,6 +25484,426 @@ function useScrubController(sink, isPlayerLive = () => false) {
24471
25484
  };
24472
25485
  }
24473
25486
  //#endregion
25487
+ //#region src/primitives/dialog.tsx
25488
+ var DialogContext = (0, react$1.createContext)(null);
25489
+ function useDialogContext() {
25490
+ const ctx = (0, react$1.useContext)(DialogContext);
25491
+ if (!ctx) throw new Error("Dialog compound components must be used within <Dialog>");
25492
+ return ctx;
25493
+ }
25494
+ function Dialog({ children, open: controlledOpen, onOpenChange }) {
25495
+ const [uncontrolledOpen, setUncontrolledOpen] = (0, react$1.useState)(false);
25496
+ const open = controlledOpen ?? uncontrolledOpen;
25497
+ const contentId = (0, react$1.useId)();
25498
+ const setOpen = (0, react$1.useCallback)((next) => {
25499
+ onOpenChange?.(next);
25500
+ if (controlledOpen === void 0) setUncontrolledOpen(next);
25501
+ }, [controlledOpen, onOpenChange]);
25502
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DialogContext.Provider, {
25503
+ value: {
25504
+ open,
25505
+ setOpen,
25506
+ contentId
25507
+ },
25508
+ children
25509
+ });
25510
+ }
25511
+ function DialogTrigger({ children, ...props }) {
25512
+ const { setOpen } = useDialogContext();
25513
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
25514
+ type: "button",
25515
+ onClick: () => setOpen(true),
25516
+ ...props,
25517
+ children
25518
+ });
25519
+ }
25520
+ /**
25521
+ * A native <dialog> opened with showModal() is promoted to the top layer, so
25522
+ * ancestor transform/overflow can't clip it — but it relies on the UA
25523
+ * stylesheet's `dialog { margin: auto }` for modal centering. Tailwind's
25524
+ * preflight resets `margin: 0` on every element, which defeats that centering
25525
+ * and pins the modal to the top-left. We therefore center deterministically:
25526
+ * a top-layer dialog with `position: fixed` is viewport-relative, and a
25527
+ * self-transform is fine, so `fixed left-1/2 top-1/2 -translate-x-1/2
25528
+ * -translate-y-1/2 m-0` always centers regardless of any margin reset. Width
25529
+ * is capped to the viewport (`w-[calc(100vw-2rem)]`) rather than a bare
25530
+ * `w-full` (which would overflow/clip a centered dialog); the per-variant
25531
+ * `max-w-*` keeps small dialogs small while the cap prevents overflow.
25532
+ *
25533
+ * Height is capped against the SAFE viewport, not the raw one. A top-layer
25534
+ * `fixed` dialog is viewport-relative, and under `viewport-fit=cover` the
25535
+ * viewport extends behind the iOS notch/Dynamic Island and home indicator —
25536
+ * so a dialog tall enough to hit its `max-h` put its header under the clock.
25537
+ * Because the box is centred on the viewport (not on the safe area), the cap
25538
+ * has to subtract TWICE the LARGER of the two insets for the box to clear
25539
+ * both edges; using each inset once would still overflow the taller side
25540
+ * whenever they are asymmetric (they are: ~59px top vs ~34px bottom). Every
25541
+ * term collapses to the previous `100dvh - 2rem` where the insets are 0.
25542
+ */
25543
+ var contentVariants = cva("fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 m-0 w-[calc(100vw-2rem)] bg-background-elevated text-foreground border border-border rounded-lg p-4 backdrop:bg-black/50 backdrop:backdrop-blur-sm max-h-[calc(100dvh-2rem-2*max(env(safe-area-inset-top),env(safe-area-inset-bottom)))] [&:not([open])]:hidden", {
25544
+ variants: {
25545
+ width: {
25546
+ sm: "max-w-sm",
25547
+ md: "max-w-md",
25548
+ lg: "max-w-lg"
25549
+ },
25550
+ /**
25551
+ * WHICH box scrolls when the content is taller than the height cap.
25552
+ *
25553
+ * `content` (default) scrolls the whole dialog: the title, any tab strip
25554
+ * and the Save/Cancel row travel with the fields. That is fine for a
25555
+ * short form and wrong for a long one — the operator scrolls to the
25556
+ * bottom of a rule editor and the buttons are somewhere above him, the
25557
+ * tab strip is gone, and the only way back to either is to scroll the
25558
+ * form he was reading.
25559
+ *
25560
+ * `body` makes the dialog a flex column that does not scroll at all and
25561
+ * hands the scrolling to `<DialogBody>`. The chrome around the body —
25562
+ * header, tab strip, footer — stays put at every height. The dialog
25563
+ * keeps its own padding either way, so `sheet-on-mobile`'s
25564
+ * safe-area bottom padding still applies to the footer.
25565
+ *
25566
+ * `body` also takes a DEFINITE height below `md` — see the
25567
+ * `scroll: 'body'` compound variants. That is not a style choice; the
25568
+ * flex chain does not resolve without it on iOS Safari.
25569
+ */
25570
+ scroll: {
25571
+ content: "overflow-y-auto",
25572
+ body: "flex flex-col overflow-hidden"
25573
+ },
25574
+ placement: {
25575
+ center: "",
25576
+ "sheet-on-mobile": "bottom-0 left-0 right-0 top-auto w-full max-w-full translate-x-0 translate-y-0 rounded-b-none max-h-[92dvh] pb-[calc(1rem+env(safe-area-inset-bottom))] md:left-1/2 md:top-1/2 md:-translate-x-1/2 md:-translate-y-1/2 md:rounded-lg md:bottom-auto md:pb-4 md:max-h-[calc(100dvh-2rem-2*max(env(safe-area-inset-top),env(safe-area-inset-bottom)))]"
25577
+ }
25578
+ },
25579
+ compoundVariants: [
25580
+ {
25581
+ placement: "sheet-on-mobile",
25582
+ width: "sm",
25583
+ class: "md:w-[calc(100vw-2rem)] md:max-w-sm"
25584
+ },
25585
+ {
25586
+ placement: "sheet-on-mobile",
25587
+ width: "md",
25588
+ class: "md:w-[calc(100vw-2rem)] md:max-w-md"
25589
+ },
25590
+ {
25591
+ placement: "sheet-on-mobile",
25592
+ width: "lg",
25593
+ class: "md:w-[calc(100vw-2rem)] md:max-w-lg"
25594
+ },
25595
+ {
25596
+ scroll: "body",
25597
+ placement: "sheet-on-mobile",
25598
+ class: "h-[92dvh] md:h-auto"
25599
+ },
25600
+ {
25601
+ scroll: "body",
25602
+ placement: "center",
25603
+ class: "h-[calc(100dvh-2rem-2*max(env(safe-area-inset-top),env(safe-area-inset-bottom)))] md:h-auto"
25604
+ }
25605
+ ],
25606
+ defaultVariants: {
25607
+ width: "md",
25608
+ placement: "center",
25609
+ scroll: "content"
25610
+ }
25611
+ });
25612
+ var DialogContent = (0, react$1.forwardRef)(({ className, width, placement, scroll, children, ...props }, ref) => {
25613
+ const { open, setOpen, contentId } = useDialogContext();
25614
+ const innerRef = (0, react$1.useRef)(null);
25615
+ const dialogRef = ref ?? innerRef;
25616
+ (0, react$1.useEffect)(() => {
25617
+ const el = dialogRef.current;
25618
+ if (!el) return;
25619
+ if (open && !el.open) el.showModal();
25620
+ if (!open && el.open) el.close();
25621
+ }, [open, dialogRef]);
25622
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dialog", {
25623
+ ref: dialogRef,
25624
+ id: contentId,
25625
+ className: cn(contentVariants({
25626
+ width,
25627
+ placement,
25628
+ scroll
25629
+ }), className),
25630
+ onClose: () => setOpen(false),
25631
+ ...props,
25632
+ children
25633
+ });
25634
+ });
25635
+ DialogContent.displayName = "DialogContent";
25636
+ /** `shrink-0` is inert outside a flex column and load-bearing inside one:
25637
+ * under `scroll="body"` the header must keep its height while the body gives
25638
+ * ground, or a long form squeezes its own title to nothing. */
25639
+ function DialogHeader({ className, ...props }) {
25640
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
25641
+ className: cn("flex flex-col gap-1 mb-3 shrink-0", className),
25642
+ ...props
25643
+ });
25644
+ }
25645
+ /**
25646
+ * The scrolling middle of a `scroll="body"` dialog.
25647
+ *
25648
+ * `min-h-0` is what makes it work: a flex item's default `min-height: auto`
25649
+ * refuses to shrink below its content, so without it the body pushes the
25650
+ * footer out of the dialog instead of scrolling — the exact failure this
25651
+ * variant exists to remove. Anything sticky INSIDE a dialog body pins against
25652
+ * this element, since it is the scroll container.
25653
+ *
25654
+ * It only bounds a scroll area if every ancestor up to the dialog has a
25655
+ * resolved height. `DialogContent` supplies the top of that chain (see the
25656
+ * `scroll: 'body'` compound variants); an intermediate wrapper between them —
25657
+ * a `<Tabs>` that owns the tab strip, say — must carry `flex-1 min-h-0` too,
25658
+ * or it collapses and takes the body with it.
25659
+ */
25660
+ function DialogBody({ className, ...props }) {
25661
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
25662
+ className: cn("flex-1 min-h-0 overflow-y-auto", className),
25663
+ ...props
25664
+ });
25665
+ }
25666
+ function DialogFooter({ className, ...props }) {
25667
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
25668
+ className: cn("flex justify-end gap-2 mt-4 shrink-0", className),
25669
+ ...props
25670
+ });
25671
+ }
25672
+ function DialogTitle({ className, ...props }) {
25673
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
25674
+ className: cn("text-sm font-semibold text-foreground", className),
25675
+ ...props
25676
+ });
25677
+ }
25678
+ function DialogDescription({ className, ...props }) {
25679
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
25680
+ className: cn("text-xs text-foreground-muted", className),
25681
+ ...props
25682
+ });
25683
+ }
25684
+ //#endregion
25685
+ //#region src/primitives/input.tsx
25686
+ /**
25687
+ * `disabled:opacity-50 disabled:cursor-not-allowed` for the same reason as
25688
+ * `Select` — an explicit `bg-surface` / `text-foreground` overrides the greying
25689
+ * a browser would otherwise apply, leaving an inert field indistinguishable
25690
+ * from a live one. See `disabled-affordance.test.tsx`.
25691
+ */
25692
+ var inputVariants = cva("h-7 w-full px-2.5 text-xs bg-surface border rounded-md text-foreground placeholder:text-foreground-subtle focus:outline-none focus:ring-1 focus:ring-primary transition-colors disabled:opacity-50 disabled:cursor-not-allowed", {
25693
+ variants: { state: {
25694
+ default: "border-border",
25695
+ error: "border-danger"
25696
+ } },
25697
+ defaultVariants: { state: "default" }
25698
+ });
25699
+ var Input = (0, react$1.forwardRef)(({ className, state, leftSlot, rightSlot, ...props }, ref) => {
25700
+ if (leftSlot || rightSlot) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
25701
+ className: "relative flex items-center w-full",
25702
+ children: [
25703
+ leftSlot && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
25704
+ className: "absolute left-2.5 flex items-center pointer-events-none",
25705
+ children: leftSlot
25706
+ }),
25707
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
25708
+ ref,
25709
+ className: cn(inputVariants({ state }), leftSlot ? "pl-7" : "", rightSlot ? "pr-7" : "", className),
25710
+ ...props
25711
+ }),
25712
+ rightSlot && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
25713
+ className: "absolute right-2.5 flex items-center pointer-events-none",
25714
+ children: rightSlot
25715
+ })
25716
+ ]
25717
+ });
25718
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
25719
+ ref,
25720
+ className: cn(inputVariants({ state }), className),
25721
+ ...props
25722
+ });
25723
+ });
25724
+ Input.displayName = "Input";
25725
+ //#endregion
25726
+ //#region src/composites/training/training-collection-picker.tsx
25727
+ /**
25728
+ * <TrainingCollectionPicker> — choose a named training collection, or create
25729
+ * one in place.
25730
+ *
25731
+ * A collection is a named set over the retrain corpus ("packages porch
25732
+ * night"). The picker lists them (newest first, with their frame counts) and
25733
+ * offers a name field for a new one; the hub refuses an empty or duplicate
25734
+ * name, and the refusal is shown here rather than swallowed.
25735
+ */
25736
+ var CREATE_REFUSAL = {
25737
+ "empty-name": "Give the collection a name.",
25738
+ "duplicate-name": "A collection with that name already exists."
25739
+ };
25740
+ function TrainingCollectionPicker({ value, onChange }) {
25741
+ const collections = usePipelineAnalyticsListRetrainCollections({});
25742
+ const create = usePipelineAnalyticsCreateRetrainCollection();
25743
+ const [name, setName] = (0, react$1.useState)("");
25744
+ const [error, setError] = (0, react$1.useState)(null);
25745
+ const list = collections.data ?? [];
25746
+ const onCreate = async () => {
25747
+ setError(null);
25748
+ const result = await create.mutateAsync({ name });
25749
+ if (result.kind === "refused") {
25750
+ setError(CREATE_REFUSAL[result.reason]);
25751
+ return;
25752
+ }
25753
+ setName("");
25754
+ await collections.refetch();
25755
+ onChange(result.collection.id);
25756
+ };
25757
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
25758
+ className: "space-y-2",
25759
+ "data-testid": "training-collection-picker",
25760
+ children: [
25761
+ list.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Select, {
25762
+ "aria-label": "Collection",
25763
+ value: value ?? "",
25764
+ options: [{
25765
+ value: "",
25766
+ label: "Choose a collection…"
25767
+ }, ...list.map((c) => ({
25768
+ value: c.id,
25769
+ label: `${c.name} (${String(c.frameCount)})`
25770
+ }))],
25771
+ onChange: (event) => {
25772
+ if (event.target.value !== "") onChange(event.target.value);
25773
+ }
25774
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
25775
+ className: "text-xs text-foreground-subtle",
25776
+ children: collections.isLoading ? "Reading the collections…" : "No collection yet — create one."
25777
+ }),
25778
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
25779
+ className: "flex items-center gap-2",
25780
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Input, {
25781
+ className: "flex-1",
25782
+ "aria-label": "New collection name",
25783
+ placeholder: "New collection (e.g. missed at night)",
25784
+ value: name,
25785
+ onChange: (event) => {
25786
+ setName(event.target.value);
25787
+ }
25788
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
25789
+ type: "button",
25790
+ variant: "secondary",
25791
+ size: "sm",
25792
+ disabled: create.isPending || name.trim() === "",
25793
+ onClick: () => void onCreate(),
25794
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Plus, { className: "h-3 w-3" }), "Create"]
25795
+ })]
25796
+ }),
25797
+ error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
25798
+ className: "text-xs text-danger",
25799
+ children: error
25800
+ })
25801
+ ]
25802
+ });
25803
+ }
25804
+ //#endregion
25805
+ //#region src/composites/training/training-refusal-text.ts
25806
+ var REFUSAL_TEXT = {
25807
+ "unknown-collection": "That collection no longer exists — pick another one.",
25808
+ "still-unavailable": "No frame could be taken from the recording at that moment.",
25809
+ "still-busy": "The recorder is busy decoding other frames. Try again in a moment.",
25810
+ "unreadable-image": "The recorder returned something that is not a picture.",
25811
+ "write-failed": "The frame could not be stored. Check the artifacts storage."
25812
+ };
25813
+ /** One sentence for a refusal, with the hub's own detail appended. */
25814
+ function trainingRefusalText(reason, detail) {
25815
+ const base = REFUSAL_TEXT[reason];
25816
+ return detail !== void 0 && detail !== "" ? `${base} (${detail})` : base;
25817
+ }
25818
+ //#endregion
25819
+ //#region src/composites/training/send-frame-to-training.tsx
25820
+ /**
25821
+ * <SendFrameToTrainingButton> — "Send frame to training" from a recording
25822
+ * playhead.
25823
+ *
25824
+ * The frame a detector most needs to learn from is often one where it saw
25825
+ * NOTHING — no event, no track, nothing to mark. From the recording, the
25826
+ * operator stops on that moment and sends it: the hub re-decodes the native
25827
+ * frame at the playhead instant and adds it to a named training collection,
25828
+ * where the Retrain page annotates it like any other frame.
25829
+ *
25830
+ * Draws NOTHING for a session that cannot curate training data: the
25831
+ * collection list is admin-only, and a button that 403s is worse than no
25832
+ * button. It is disabled, with the reason, while there is no playhead.
25833
+ */
25834
+ function SendFrameToTrainingButton({ deviceId, epochMs }) {
25835
+ const collections = usePipelineAnalyticsListRetrainCollections({}, { retry: false });
25836
+ const send = usePipelineAnalyticsAddRetrainFrameFromRecording();
25837
+ const [open, setOpen] = (0, react$1.useState)(false);
25838
+ const [collectionId, setCollectionId] = (0, react$1.useState)(null);
25839
+ const [instant, setInstant] = (0, react$1.useState)(null);
25840
+ const [outcome, setOutcome] = (0, react$1.useState)(null);
25841
+ if (collections.isError) return null;
25842
+ const onSend = async () => {
25843
+ if (collectionId === null || instant === null) return;
25844
+ const result = await send.mutateAsync({
25845
+ collectionId,
25846
+ deviceId,
25847
+ epochMs: instant
25848
+ });
25849
+ const name = collections.data?.find((c) => c.id === collectionId)?.name ?? "the collection";
25850
+ setOutcome(result.kind === "added" ? {
25851
+ kind: "added",
25852
+ reused: result.reused,
25853
+ collection: name
25854
+ } : {
25855
+ kind: "refused",
25856
+ text: trainingRefusalText(result.reason, result.detail)
25857
+ });
25858
+ if (result.kind === "added") await collections.refetch();
25859
+ };
25860
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
25861
+ type: "button",
25862
+ variant: "secondary",
25863
+ size: "sm",
25864
+ "data-testid": "send-frame-to-training",
25865
+ disabled: epochMs === null,
25866
+ title: epochMs === null ? "Play back a recording first — the frame at the playhead is the one sent" : "Add the frame at the playhead to a training collection",
25867
+ onClick: () => {
25868
+ setInstant(epochMs);
25869
+ setOutcome(null);
25870
+ setOpen(true);
25871
+ },
25872
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(GraduationCap, { className: "h-3.5 w-3.5" }), "Send frame to training"]
25873
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Dialog, {
25874
+ open,
25875
+ onOpenChange: (next) => !send.isPending && setOpen(next),
25876
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DialogContent, { children: [
25877
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DialogHeader, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DialogTitle, { children: "Send this frame to training" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DialogDescription, { children: instant !== null ? `The camera's frame at ${new Date(instant).toLocaleString()} is taken from the recording at full resolution and added to the collection. Annotate it on the Retrain page — including what the model missed.` : "" })] }),
25878
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TrainingCollectionPicker, {
25879
+ value: collectionId,
25880
+ onChange: setCollectionId
25881
+ }),
25882
+ outcome !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
25883
+ className: `mt-2 text-xs ${outcome.kind === "added" ? "text-success" : "text-danger"}`,
25884
+ role: "status",
25885
+ children: outcome.kind === "added" ? outcome.reused ? `That moment was already in the dataset; it is now in ${outcome.collection}.` : `Added to ${outcome.collection}.` : outcome.text
25886
+ }),
25887
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DialogFooter, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
25888
+ type: "button",
25889
+ variant: "ghost",
25890
+ size: "sm",
25891
+ disabled: send.isPending,
25892
+ onClick: () => {
25893
+ setOpen(false);
25894
+ },
25895
+ children: outcome?.kind === "added" ? "Done" : "Cancel"
25896
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
25897
+ type: "button",
25898
+ size: "sm",
25899
+ disabled: collectionId === null || send.isPending || outcome?.kind === "added",
25900
+ onClick: () => void onSend(),
25901
+ children: [send.isPending && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LoaderCircle, { className: "h-3.5 w-3.5 animate-spin" }), "Send"]
25902
+ })] })
25903
+ ] })
25904
+ })] });
25905
+ }
25906
+ //#endregion
24474
25907
  //#region src/composites/cap-settings/CoverageTrack.tsx
24475
25908
  function CoverageTrack({ windowFrom, windowTo, spans, playheadMs, onSeek, onScrubStart, onScrubMove, onScrubEnd }) {
24476
25909
  const [hoverInfo, setHoverInfo] = (0, react$1.useState)(null);
@@ -25049,6 +26482,13 @@ function RecordingTimeline({ deviceId }) {
25049
26482
  dayBounds: computedDayBounds,
25050
26483
  onViewChange: handleViewChange
25051
26484
  }),
26485
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
26486
+ className: "flex justify-end",
26487
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SendFrameToTrainingButton, {
26488
+ deviceId,
26489
+ epochMs: displayPlayheadMs
26490
+ })
26491
+ }),
25052
26492
  recorded.channelOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
25053
26493
  "data-testid": "frame-push-status-row",
25054
26494
  className: "flex flex-wrap items-center gap-2 text-[10px]",
@@ -26646,6 +28086,32 @@ function FieldWrapper({ label, description, required, span, children, translatio
26646
28086
  ]
26647
28087
  });
26648
28088
  }
28089
+ /**
28090
+ * A keychain-stored secret (D787) arrives as its VIEW — `{ $secret, set }` —
28091
+ * or `null`, never as a value; it renders as "set · Change · Clear · Reveal".
28092
+ * Decided ONCE, at mount, from what the form was hydrated with: once the
28093
+ * operator types, the value is a plain string and the field must stay the
28094
+ * same control. A form that was not hydrated from a keychain-aware store (a
28095
+ * create dialog seeding `''`) keeps the plain input.
28096
+ */
28097
+ function KeychainOr({ field, value, onChange, disabled, translationFn, multiline, children }) {
28098
+ const [keychain] = (0, react$1.useState)(() => (0, _camstack_types.isKeychainStoredField)(field) && isKeychainFieldValue(value));
28099
+ if (!keychain) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children });
28100
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldWrapper, {
28101
+ label: "label" in field ? field.label : void 0,
28102
+ description: "description" in field ? field.description : void 0,
28103
+ required: "required" in field ? field.required : void 0,
28104
+ span: "span" in field ? field.span : void 0,
28105
+ translationFn,
28106
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SecretFieldControl, {
28107
+ field,
28108
+ value,
28109
+ onChange,
28110
+ disabled,
28111
+ multiline
28112
+ })
28113
+ });
28114
+ }
26649
28115
  function TextField({ field, value, onChange, disabled, translationFn }) {
26650
28116
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldWrapper, {
26651
28117
  label: field.label,
@@ -27166,6 +28632,23 @@ function ColorField({ field, value, onChange, disabled, translationFn }) {
27166
28632
  })]
27167
28633
  });
27168
28634
  }
28635
+ function IconField({ field, value, onChange, disabled, translationFn }) {
28636
+ const defaultKey = typeof field.default === "string" ? field.default : "";
28637
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldWrapper, {
28638
+ label: field.label,
28639
+ description: field.description,
28640
+ required: field.required,
28641
+ span: field.span,
28642
+ translationFn,
28643
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfigIconFieldControl, {
28644
+ label: resolveLabel(field.label, translationFn) ?? field.label,
28645
+ value,
28646
+ defaultKey,
28647
+ disabled: disabled || field.disabled,
28648
+ onChange
28649
+ })
28650
+ });
28651
+ }
27169
28652
  function ProbeField({ field, value, onChange, disabled, translationFn, onTestField, onAction, externalProbe }) {
27170
28653
  const [localStatus, setLocalStatus] = (0, react$1.useState)("idle");
27171
28654
  const [localResult, setLocalResult] = (0, react$1.useState)(null);
@@ -27701,7 +29184,7 @@ function resolveActionParams(field, values) {
27701
29184
  const params = { ...field.paramsStatic };
27702
29185
  if (field.paramsFromForm) for (const [paramKey, formKey] of Object.entries(field.paramsFromForm)) {
27703
29186
  const v = values[formKey];
27704
- if (v !== void 0 && v !== null && v !== "") params[paramKey] = v;
29187
+ if (v !== void 0 && v !== null && v !== "" && !(0, _camstack_types.isSecretClear)(v)) params[paramKey] = v;
27705
29188
  }
27706
29189
  return params;
27707
29190
  }
@@ -27800,8 +29283,16 @@ function AddonActionButtonField({ field, values, disabled, onAction }) {
27800
29283
  const [loading, setLoading] = (0, react$1.useState)(false);
27801
29284
  const [status, setStatus] = (0, react$1.useState)(null);
27802
29285
  const confirm = useConfirm();
29286
+ const prompt = usePrompt();
27803
29287
  const deviceId = useDeviceId();
27804
29288
  const handleClick = async () => {
29289
+ const asked = field.prompt ? await prompt({
29290
+ title: field.prompt.title ?? field.label,
29291
+ ...field.prompt.message !== void 0 ? { message: field.prompt.message } : {},
29292
+ ...field.prompt.placeholder !== void 0 ? { placeholder: field.prompt.placeholder } : {},
29293
+ confirmLabel: field.buttonLabel
29294
+ }) : null;
29295
+ if (field.prompt && asked === null) return;
27805
29296
  if (field.confirmMessage) {
27806
29297
  if (!await confirm({
27807
29298
  title: field.label,
@@ -27812,6 +29303,7 @@ function AddonActionButtonField({ field, values, disabled, onAction }) {
27812
29303
  }
27813
29304
  if (!onAction) return;
27814
29305
  const params = resolveActionParams(field, values);
29306
+ if (field.prompt && asked !== null) params[field.prompt.param] = asked;
27815
29307
  if (field.includeDeviceId === true && deviceId !== null && deviceId !== void 0) params["deviceId"] = deviceId;
27816
29308
  setLoading(true);
27817
29309
  setStatus(null);
@@ -28188,12 +29680,19 @@ function FormField({ field, values, onChange, disabled, translationFn, onTestFie
28188
29680
  const handleChange = (v) => onChange(field.key, v);
28189
29681
  const customRenderer = useCustomFieldRenderer(field.type);
28190
29682
  switch (field.type) {
28191
- case "text": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
29683
+ case "text": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KeychainOr, {
28192
29684
  field,
28193
29685
  value,
28194
29686
  onChange: handleChange,
28195
29687
  disabled,
28196
- translationFn
29688
+ translationFn,
29689
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
29690
+ field,
29691
+ value,
29692
+ onChange: handleChange,
29693
+ disabled,
29694
+ translationFn
29695
+ })
28197
29696
  });
28198
29697
  case "number": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NumberField, {
28199
29698
  field,
@@ -28237,19 +29736,34 @@ function FormField({ field, values, onChange, disabled, translationFn, onTestFie
28237
29736
  disabled,
28238
29737
  translationFn
28239
29738
  });
28240
- case "password": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PasswordField, {
29739
+ case "password": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KeychainOr, {
28241
29740
  field,
28242
29741
  value,
28243
29742
  onChange: handleChange,
28244
29743
  disabled,
28245
- translationFn
29744
+ translationFn,
29745
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PasswordField, {
29746
+ field,
29747
+ value,
29748
+ onChange: handleChange,
29749
+ disabled,
29750
+ translationFn
29751
+ })
28246
29752
  });
28247
- case "textarea": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextAreaField, {
29753
+ case "textarea": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KeychainOr, {
28248
29754
  field,
28249
29755
  value,
28250
29756
  onChange: handleChange,
28251
29757
  disabled,
28252
- translationFn
29758
+ translationFn,
29759
+ multiline: true,
29760
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextAreaField, {
29761
+ field,
29762
+ value,
29763
+ onChange: handleChange,
29764
+ disabled,
29765
+ translationFn
29766
+ })
28253
29767
  });
28254
29768
  case "slider": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SliderField, {
28255
29769
  field,
@@ -28272,6 +29786,13 @@ function FormField({ field, values, onChange, disabled, translationFn, onTestFie
28272
29786
  disabled,
28273
29787
  translationFn
28274
29788
  });
29789
+ case "icon": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconField, {
29790
+ field,
29791
+ value,
29792
+ onChange: handleChange,
29793
+ disabled,
29794
+ translationFn
29795
+ });
28275
29796
  case "node-select": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NodeSelectField, {
28276
29797
  field,
28277
29798
  value,
@@ -30044,47 +31565,6 @@ var Separator = (0, react$1.forwardRef)(({ className, orientation, ...props }, r
30044
31565
  });
30045
31566
  Separator.displayName = "Separator";
30046
31567
  //#endregion
30047
- //#region src/primitives/input.tsx
30048
- /**
30049
- * `disabled:opacity-50 disabled:cursor-not-allowed` for the same reason as
30050
- * `Select` — an explicit `bg-surface` / `text-foreground` overrides the greying
30051
- * a browser would otherwise apply, leaving an inert field indistinguishable
30052
- * from a live one. See `disabled-affordance.test.tsx`.
30053
- */
30054
- var inputVariants = cva("h-7 w-full px-2.5 text-xs bg-surface border rounded-md text-foreground placeholder:text-foreground-subtle focus:outline-none focus:ring-1 focus:ring-primary transition-colors disabled:opacity-50 disabled:cursor-not-allowed", {
30055
- variants: { state: {
30056
- default: "border-border",
30057
- error: "border-danger"
30058
- } },
30059
- defaultVariants: { state: "default" }
30060
- });
30061
- var Input = (0, react$1.forwardRef)(({ className, state, leftSlot, rightSlot, ...props }, ref) => {
30062
- if (leftSlot || rightSlot) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
30063
- className: "relative flex items-center w-full",
30064
- children: [
30065
- leftSlot && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
30066
- className: "absolute left-2.5 flex items-center pointer-events-none",
30067
- children: leftSlot
30068
- }),
30069
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
30070
- ref,
30071
- className: cn(inputVariants({ state }), leftSlot ? "pl-7" : "", rightSlot ? "pr-7" : "", className),
30072
- ...props
30073
- }),
30074
- rightSlot && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
30075
- className: "absolute right-2.5 flex items-center pointer-events-none",
30076
- children: rightSlot
30077
- })
30078
- ]
30079
- });
30080
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
30081
- ref,
30082
- className: cn(inputVariants({ state }), className),
30083
- ...props
30084
- });
30085
- });
30086
- Input.displayName = "Input";
30087
- //#endregion
30088
31568
  //#region src/primitives/checkbox.tsx
30089
31569
  var Checkbox = (0, react$1.forwardRef)(({ className, ...props }, ref) => {
30090
31570
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
@@ -30117,204 +31597,6 @@ var Switch = (0, react$1.forwardRef)(({ className, checked, onCheckedChange, lab
30117
31597
  });
30118
31598
  Switch.displayName = "Switch";
30119
31599
  //#endregion
30120
- //#region src/primitives/dialog.tsx
30121
- var DialogContext = (0, react$1.createContext)(null);
30122
- function useDialogContext() {
30123
- const ctx = (0, react$1.useContext)(DialogContext);
30124
- if (!ctx) throw new Error("Dialog compound components must be used within <Dialog>");
30125
- return ctx;
30126
- }
30127
- function Dialog({ children, open: controlledOpen, onOpenChange }) {
30128
- const [uncontrolledOpen, setUncontrolledOpen] = (0, react$1.useState)(false);
30129
- const open = controlledOpen ?? uncontrolledOpen;
30130
- const contentId = (0, react$1.useId)();
30131
- const setOpen = (0, react$1.useCallback)((next) => {
30132
- onOpenChange?.(next);
30133
- if (controlledOpen === void 0) setUncontrolledOpen(next);
30134
- }, [controlledOpen, onOpenChange]);
30135
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DialogContext.Provider, {
30136
- value: {
30137
- open,
30138
- setOpen,
30139
- contentId
30140
- },
30141
- children
30142
- });
30143
- }
30144
- function DialogTrigger({ children, ...props }) {
30145
- const { setOpen } = useDialogContext();
30146
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
30147
- type: "button",
30148
- onClick: () => setOpen(true),
30149
- ...props,
30150
- children
30151
- });
30152
- }
30153
- /**
30154
- * A native <dialog> opened with showModal() is promoted to the top layer, so
30155
- * ancestor transform/overflow can't clip it — but it relies on the UA
30156
- * stylesheet's `dialog { margin: auto }` for modal centering. Tailwind's
30157
- * preflight resets `margin: 0` on every element, which defeats that centering
30158
- * and pins the modal to the top-left. We therefore center deterministically:
30159
- * a top-layer dialog with `position: fixed` is viewport-relative, and a
30160
- * self-transform is fine, so `fixed left-1/2 top-1/2 -translate-x-1/2
30161
- * -translate-y-1/2 m-0` always centers regardless of any margin reset. Width
30162
- * is capped to the viewport (`w-[calc(100vw-2rem)]`) rather than a bare
30163
- * `w-full` (which would overflow/clip a centered dialog); the per-variant
30164
- * `max-w-*` keeps small dialogs small while the cap prevents overflow.
30165
- *
30166
- * Height is capped against the SAFE viewport, not the raw one. A top-layer
30167
- * `fixed` dialog is viewport-relative, and under `viewport-fit=cover` the
30168
- * viewport extends behind the iOS notch/Dynamic Island and home indicator —
30169
- * so a dialog tall enough to hit its `max-h` put its header under the clock.
30170
- * Because the box is centred on the viewport (not on the safe area), the cap
30171
- * has to subtract TWICE the LARGER of the two insets for the box to clear
30172
- * both edges; using each inset once would still overflow the taller side
30173
- * whenever they are asymmetric (they are: ~59px top vs ~34px bottom). Every
30174
- * term collapses to the previous `100dvh - 2rem` where the insets are 0.
30175
- */
30176
- var contentVariants = cva("fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 m-0 w-[calc(100vw-2rem)] bg-background-elevated text-foreground border border-border rounded-lg p-4 backdrop:bg-black/50 backdrop:backdrop-blur-sm max-h-[calc(100dvh-2rem-2*max(env(safe-area-inset-top),env(safe-area-inset-bottom)))] [&:not([open])]:hidden", {
30177
- variants: {
30178
- width: {
30179
- sm: "max-w-sm",
30180
- md: "max-w-md",
30181
- lg: "max-w-lg"
30182
- },
30183
- /**
30184
- * WHICH box scrolls when the content is taller than the height cap.
30185
- *
30186
- * `content` (default) scrolls the whole dialog: the title, any tab strip
30187
- * and the Save/Cancel row travel with the fields. That is fine for a
30188
- * short form and wrong for a long one — the operator scrolls to the
30189
- * bottom of a rule editor and the buttons are somewhere above him, the
30190
- * tab strip is gone, and the only way back to either is to scroll the
30191
- * form he was reading.
30192
- *
30193
- * `body` makes the dialog a flex column that does not scroll at all and
30194
- * hands the scrolling to `<DialogBody>`. The chrome around the body —
30195
- * header, tab strip, footer — stays put at every height. The dialog
30196
- * keeps its own padding either way, so `sheet-on-mobile`'s
30197
- * safe-area bottom padding still applies to the footer.
30198
- *
30199
- * `body` also takes a DEFINITE height below `md` — see the
30200
- * `scroll: 'body'` compound variants. That is not a style choice; the
30201
- * flex chain does not resolve without it on iOS Safari.
30202
- */
30203
- scroll: {
30204
- content: "overflow-y-auto",
30205
- body: "flex flex-col overflow-hidden"
30206
- },
30207
- placement: {
30208
- center: "",
30209
- "sheet-on-mobile": "bottom-0 left-0 right-0 top-auto w-full max-w-full translate-x-0 translate-y-0 rounded-b-none max-h-[92dvh] pb-[calc(1rem+env(safe-area-inset-bottom))] md:left-1/2 md:top-1/2 md:-translate-x-1/2 md:-translate-y-1/2 md:rounded-lg md:bottom-auto md:pb-4 md:max-h-[calc(100dvh-2rem-2*max(env(safe-area-inset-top),env(safe-area-inset-bottom)))]"
30210
- }
30211
- },
30212
- compoundVariants: [
30213
- {
30214
- placement: "sheet-on-mobile",
30215
- width: "sm",
30216
- class: "md:w-[calc(100vw-2rem)] md:max-w-sm"
30217
- },
30218
- {
30219
- placement: "sheet-on-mobile",
30220
- width: "md",
30221
- class: "md:w-[calc(100vw-2rem)] md:max-w-md"
30222
- },
30223
- {
30224
- placement: "sheet-on-mobile",
30225
- width: "lg",
30226
- class: "md:w-[calc(100vw-2rem)] md:max-w-lg"
30227
- },
30228
- {
30229
- scroll: "body",
30230
- placement: "sheet-on-mobile",
30231
- class: "h-[92dvh] md:h-auto"
30232
- },
30233
- {
30234
- scroll: "body",
30235
- placement: "center",
30236
- class: "h-[calc(100dvh-2rem-2*max(env(safe-area-inset-top),env(safe-area-inset-bottom)))] md:h-auto"
30237
- }
30238
- ],
30239
- defaultVariants: {
30240
- width: "md",
30241
- placement: "center",
30242
- scroll: "content"
30243
- }
30244
- });
30245
- var DialogContent = (0, react$1.forwardRef)(({ className, width, placement, scroll, children, ...props }, ref) => {
30246
- const { open, setOpen, contentId } = useDialogContext();
30247
- const innerRef = (0, react$1.useRef)(null);
30248
- const dialogRef = ref ?? innerRef;
30249
- (0, react$1.useEffect)(() => {
30250
- const el = dialogRef.current;
30251
- if (!el) return;
30252
- if (open && !el.open) el.showModal();
30253
- if (!open && el.open) el.close();
30254
- }, [open, dialogRef]);
30255
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dialog", {
30256
- ref: dialogRef,
30257
- id: contentId,
30258
- className: cn(contentVariants({
30259
- width,
30260
- placement,
30261
- scroll
30262
- }), className),
30263
- onClose: () => setOpen(false),
30264
- ...props,
30265
- children
30266
- });
30267
- });
30268
- DialogContent.displayName = "DialogContent";
30269
- /** `shrink-0` is inert outside a flex column and load-bearing inside one:
30270
- * under `scroll="body"` the header must keep its height while the body gives
30271
- * ground, or a long form squeezes its own title to nothing. */
30272
- function DialogHeader({ className, ...props }) {
30273
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
30274
- className: cn("flex flex-col gap-1 mb-3 shrink-0", className),
30275
- ...props
30276
- });
30277
- }
30278
- /**
30279
- * The scrolling middle of a `scroll="body"` dialog.
30280
- *
30281
- * `min-h-0` is what makes it work: a flex item's default `min-height: auto`
30282
- * refuses to shrink below its content, so without it the body pushes the
30283
- * footer out of the dialog instead of scrolling — the exact failure this
30284
- * variant exists to remove. Anything sticky INSIDE a dialog body pins against
30285
- * this element, since it is the scroll container.
30286
- *
30287
- * It only bounds a scroll area if every ancestor up to the dialog has a
30288
- * resolved height. `DialogContent` supplies the top of that chain (see the
30289
- * `scroll: 'body'` compound variants); an intermediate wrapper between them —
30290
- * a `<Tabs>` that owns the tab strip, say — must carry `flex-1 min-h-0` too,
30291
- * or it collapses and takes the body with it.
30292
- */
30293
- function DialogBody({ className, ...props }) {
30294
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
30295
- className: cn("flex-1 min-h-0 overflow-y-auto", className),
30296
- ...props
30297
- });
30298
- }
30299
- function DialogFooter({ className, ...props }) {
30300
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
30301
- className: cn("flex justify-end gap-2 mt-4 shrink-0", className),
30302
- ...props
30303
- });
30304
- }
30305
- function DialogTitle({ className, ...props }) {
30306
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
30307
- className: cn("text-sm font-semibold text-foreground", className),
30308
- ...props
30309
- });
30310
- }
30311
- function DialogDescription({ className, ...props }) {
30312
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
30313
- className: cn("text-xs text-foreground-muted", className),
30314
- ...props
30315
- });
30316
- }
30317
- //#endregion
30318
31600
  //#region src/primitives/tooltip.tsx
30319
31601
  var TooltipContext = (0, react$1.createContext)(null);
30320
31602
  function useTooltipContext() {
@@ -49421,1248 +50703,683 @@ function SwitchInlineControl({ trpc, deviceId, features, optimistic }) {
49421
50703
  optimistic
49422
50704
  });
49423
50705
  }
49424
- function useLockCapGuard(trpc, deviceId) {
49425
- const { cap } = useDeviceCapability({
49426
- trpc,
49427
- deviceId,
49428
- cap: (d) => d.lockControl,
49429
- slice: (d) => d.state.lockControl
49430
- });
49431
- return cap !== void 0;
49432
- }
49433
- function LockInlineControl({ trpc, deviceId, features, optimistic }) {
49434
- if (!useLockCapGuard(trpc, deviceId)) return null;
49435
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LockPanel, {
49436
- trpc,
49437
- deviceId,
49438
- features,
49439
- layout: "compact",
49440
- optimistic
49441
- });
49442
- }
49443
- function LightInlineControl({ trpc, deviceId, features, optimistic }) {
49444
- if (!useSwitchCapGuard(trpc, deviceId)) return null;
49445
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
49446
- className: "inline-flex items-center gap-2",
49447
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SwitchPanel, {
49448
- trpc,
49449
- deviceId,
49450
- features,
49451
- layout: "compact",
49452
- optimistic
49453
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BrightnessPanel, {
49454
- trpc,
49455
- deviceId,
49456
- features,
49457
- layout: "compact",
49458
- optimistic
49459
- })]
49460
- });
49461
- }
49462
- function useFanCapGuard(trpc, deviceId) {
49463
- const { cap } = useDeviceCapability({
49464
- trpc,
49465
- deviceId,
49466
- cap: (d) => d.fanControl,
49467
- slice: (d) => d.state.fanControl
49468
- });
49469
- return cap !== void 0;
49470
- }
49471
- function FanInlineControl({ trpc, deviceId, features, optimistic }) {
49472
- if (!useFanCapGuard(trpc, deviceId)) return null;
49473
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
49474
- className: "inline-flex items-center gap-2",
49475
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SwitchPanel, {
49476
- trpc,
49477
- deviceId,
49478
- features,
49479
- layout: "compact",
49480
- optimistic
49481
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FanPanel, {
49482
- trpc,
49483
- deviceId,
49484
- features,
49485
- layout: "compact",
49486
- optimistic
49487
- })]
49488
- });
49489
- }
49490
- function useClimateCapGuard(trpc, deviceId) {
49491
- const { cap } = useDeviceCapability({
49492
- trpc,
49493
- deviceId,
49494
- cap: (d) => d.climateControl,
49495
- slice: (d) => d.state.climateControl
49496
- });
49497
- return cap !== void 0;
49498
- }
49499
- function ThermostatInlineControl({ trpc, deviceId, features, optimistic }) {
49500
- if (!useClimateCapGuard(trpc, deviceId)) return null;
49501
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ClimatePanel, {
49502
- trpc,
49503
- deviceId,
49504
- features,
49505
- layout: "compact",
49506
- optimistic
49507
- });
49508
- }
49509
- function AcInlineControl({ trpc, deviceId, features, optimistic }) {
49510
- if (!useClimateCapGuard(trpc, deviceId)) return null;
49511
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AcPanel, {
49512
- trpc,
49513
- deviceId,
49514
- features,
49515
- layout: "compact",
49516
- optimistic
49517
- });
49518
- }
49519
- function AcHeroCard({ trpc, deviceId, features, optimistic }) {
49520
- if (!useClimateCapGuard(trpc, deviceId)) return null;
49521
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AcPanel, {
49522
- trpc,
49523
- deviceId,
49524
- features,
49525
- layout: "full",
49526
- optimistic
49527
- });
49528
- }
49529
- function useMediaPlayerCapGuard(trpc, deviceId) {
49530
- const { cap } = useDeviceCapability({
49531
- trpc,
49532
- deviceId,
49533
- cap: (d) => d.mediaPlayer,
49534
- slice: (d) => d.state.mediaPlayer
49535
- });
49536
- return cap !== void 0;
49537
- }
49538
- function MediaPlayerInlineControl({ trpc, deviceId, features, optimistic }) {
49539
- if (!useMediaPlayerCapGuard(trpc, deviceId)) return null;
49540
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MediaPlayerPanel, {
49541
- trpc,
49542
- deviceId,
49543
- features,
49544
- layout: "compact",
49545
- optimistic
49546
- });
49547
- }
49548
- function useControlCapGuard(trpc, deviceId) {
49549
- const { cap } = useDeviceCapability({
49550
- trpc,
49551
- deviceId,
49552
- cap: (d) => d.control,
49553
- slice: (d) => d.state.control
49554
- });
49555
- return cap !== void 0;
49556
- }
49557
- function ControlInlineControl({ trpc, deviceId, features, optimistic }) {
49558
- if (!useControlCapGuard(trpc, deviceId)) return null;
49559
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ControlPanel, {
49560
- trpc,
49561
- deviceId,
49562
- features,
49563
- layout: "compact",
49564
- optimistic
49565
- });
49566
- }
49567
- function ControlHeroCard({ trpc, deviceId, features, optimistic }) {
49568
- if (!useControlCapGuard(trpc, deviceId)) return null;
49569
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HeroShell, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ControlPanel, {
49570
- trpc,
49571
- deviceId,
49572
- features,
49573
- layout: "full",
49574
- optimistic
49575
- }) });
49576
- }
49577
- //#endregion
49578
- //#region src/composites/device-controls/control-registry.tsx
49579
- /**
49580
- * Presentation binding: the shared `DeviceControlKind` (decided in
49581
- * `@camstack/types` `resolveDeviceControlKind`) → this platform's web
49582
- * components. Exhaustive over `DeviceControlKind` — omitting a kind fails the
49583
- * build here. The type→kind DECISION is single-sourced in `@camstack/types`;
49584
- * this map only binds a kind to its web widgets.
49585
- */
49586
- var CONTROL_KIND_COMPONENTS = {
49587
- cover: {
49588
- InlineControl: CoverInlineControl,
49589
- HeroCard: CoverHeroCard
49590
- },
49591
- valve: {
49592
- InlineControl: ValveInlineControl,
49593
- HeroCard: ValveHeroCard
49594
- },
49595
- humidifier: {
49596
- InlineControl: HumidifierInlineControl,
49597
- HeroCard: HumidifierHeroCard
49598
- },
49599
- "water-heater": {
49600
- InlineControl: WaterHeaterInlineControl,
49601
- HeroCard: WaterHeaterHeroCard
49602
- },
49603
- switch: {
49604
- InlineControl: SwitchInlineControl,
49605
- HeroCard: SwitchHeroCard
49606
- },
49607
- light: {
49608
- InlineControl: LightInlineControl,
49609
- HeroCard: LightHeroCard
49610
- },
49611
- fan: {
49612
- InlineControl: FanInlineControl,
49613
- HeroCard: FanHeroCard
49614
- },
49615
- sensor: {
49616
- InlineControl: SensorInlineControl,
49617
- HeroCard: SensorHeroCard
49618
- },
49619
- thermostat: {
49620
- InlineControl: ThermostatInlineControl,
49621
- HeroCard: ThermostatHeroCard
49622
- },
49623
- climate: {
49624
- InlineControl: AcInlineControl,
49625
- HeroCard: AcHeroCard
49626
- },
49627
- button: {
49628
- InlineControl: ButtonControl,
49629
- HeroCard: ButtonHeroCard
49630
- },
49631
- update: {
49632
- InlineControl: UpdateInlineControl,
49633
- HeroCard: UpdateHeroCard
49634
- },
49635
- lock: {
49636
- InlineControl: LockInlineControl,
49637
- HeroCard: LockHeroCard
49638
- },
49639
- "media-player": {
49640
- InlineControl: MediaPlayerInlineControl,
49641
- HeroCard: MediaPlayerHeroCard
49642
- },
49643
- alarm: {
49644
- InlineControl: AlarmInlineControl,
49645
- HeroCard: AlarmHeroCard
49646
- },
49647
- control: {
49648
- InlineControl: ControlInlineControl,
49649
- HeroCard: ControlHeroCard
49650
- },
49651
- weather: {
49652
- InlineControl: WeatherInlineControl,
49653
- HeroCard: WeatherHeroCard
49654
- },
49655
- vacuum: {
49656
- InlineControl: VacuumInlineControl,
49657
- HeroCard: VacuumHeroCard
49658
- },
49659
- "lawn-mower": {
49660
- InlineControl: LawnMowerInlineControl,
49661
- HeroCard: LawnMowerHeroCard
49662
- },
49663
- image: {
49664
- InlineControl: ImageInlineControl,
49665
- HeroCard: ImageHeroCard
49666
- },
49667
- "pet-feeder": {
49668
- InlineControl: PetFeederInlineControl,
49669
- HeroCard: PetFeederHeroCard
49670
- },
49671
- script: {
49672
- InlineControl: ScriptInlineControl,
49673
- HeroCard: ScriptHeroCard
49674
- },
49675
- automation: {
49676
- InlineControl: AutomationInlineControl,
49677
- HeroCard: AutomationHeroCard
49678
- },
49679
- dummy: {
49680
- InlineControl: DummyInline,
49681
- HeroCard: DummyHeroCard
49682
- }
49683
- };
49684
- /**
49685
- * Exhaustive record over DeviceType, DERIVED from the shared decision
49686
- * (`DEVICE_TYPE_CONTROL_KIND`) composed with the web `CONTROL_KIND_COMPONENTS`
49687
- * binding — so the type→control mapping can never diverge from the single
49688
- * source in `@camstack/types`. Preserved as a public export for consumers/tests
49689
- * that index it by `DeviceType`.
49690
- *
49691
- * The `Object.fromEntries` result is a `{ [k: string]: DeviceTypeControl }`;
49692
- * `Object.values(DeviceType)` (a string enum) yields exactly the enum's members,
49693
- * so the record is fully populated — the assertion narrows that documented
49694
- * enum-record projection boundary.
49695
- */
49696
- var DEVICE_TYPE_CONTROL = Object.fromEntries(Object.values(_camstack_types.DeviceType).map((type) => [type, CONTROL_KIND_COMPONENTS[_camstack_types.DEVICE_TYPE_CONTROL_KIND[type]]]));
49697
- /**
49698
- * Per-type FILL flag.
49699
- *
49700
- * true — range / choice / button controls (cover ▲■▼ triad, thermostat
49701
- * setpoint stepper, media/control/button action buttons, alarm
49702
- * segmented control). The child STRETCHES to the full column width.
49703
- * false — readouts, tap-toggles, icon controls (lock), and dummy/placeholder
49704
- * types. The child keeps its NATURAL width; where it sits in the
49705
- * column (flush right vs centered) is decided by the caller via a
49706
- * `ControlPlacement` — see `resolveControlAlign`.
49707
- *
49708
- * Dummy types (Camera, Hub, Generic, Weather, Vacuum, Container, and placeholder
49709
- * types Notifier/Script/Automation whose InlineControl renders "—") are `false` —
49710
- * the dash placeholder must not be stretched to the full column width. (AlarmPanel
49711
- * has a real segmented inline control and is `true`.) When a later task gives a
49712
- * remaining placeholder type a real stretchy inline control, flip its flag to true.
49713
- *
49714
- * Exhaustive over DeviceType — a missing member is a TypeScript build error.
49715
- */
49716
- var CONTROL_FILLS = {
49717
- [_camstack_types.DeviceType.Cover]: true,
49718
- [_camstack_types.DeviceType.Valve]: true,
49719
- [_camstack_types.DeviceType.Thermostat]: true,
49720
- [_camstack_types.DeviceType.Climate]: true,
49721
- [_camstack_types.DeviceType.Control]: true,
49722
- [_camstack_types.DeviceType.Button]: true,
49723
- [_camstack_types.DeviceType.MediaPlayer]: true,
49724
- [_camstack_types.DeviceType.AlarmPanel]: true,
49725
- [_camstack_types.DeviceType.Lock]: false,
49726
- [_camstack_types.DeviceType.Vacuum]: false,
49727
- [_camstack_types.DeviceType.LawnMower]: false,
49728
- [_camstack_types.DeviceType.Humidifier]: false,
49729
- [_camstack_types.DeviceType.Switch]: false,
49730
- [_camstack_types.DeviceType.Siren]: false,
49731
- [_camstack_types.DeviceType.Light]: false,
49732
- [_camstack_types.DeviceType.Fan]: false,
49733
- [_camstack_types.DeviceType.WaterHeater]: false,
49734
- [_camstack_types.DeviceType.Sensor]: false,
49735
- [_camstack_types.DeviceType.Presence]: false,
49736
- [_camstack_types.DeviceType.EventEmitter]: false,
49737
- [_camstack_types.DeviceType.Notifier]: false,
49738
- [_camstack_types.DeviceType.Script]: false,
49739
- [_camstack_types.DeviceType.Automation]: false,
49740
- [_camstack_types.DeviceType.Camera]: false,
49741
- [_camstack_types.DeviceType.Hub]: false,
49742
- [_camstack_types.DeviceType.Update]: false,
49743
- [_camstack_types.DeviceType.Generic]: false,
49744
- [_camstack_types.DeviceType.Weather]: false,
49745
- [_camstack_types.DeviceType.Container]: false,
49746
- [_camstack_types.DeviceType.Image]: false,
49747
- [_camstack_types.DeviceType.PetFeeder]: false
49748
- };
49749
- var DEVICE_TYPE_VALUES$1 = new Set(Object.values(_camstack_types.DeviceType));
49750
- /**
49751
- * Runtime-string-safe lookup: a device row's `type` crosses the wire as a plain
49752
- * string. The type→control-kind DECISION is single-sourced in `@camstack/types`
49753
- * (`resolveDeviceControlKind`, null for an unrecognised type); this maps the
49754
- * resolved kind onto the web components.
49755
- */
49756
- function resolveDeviceControl(type) {
49757
- const kind = (0, _camstack_types.resolveDeviceControlKind)(type);
49758
- return kind ? CONTROL_KIND_COMPONENTS[kind] : null;
49759
- }
49760
- /**
49761
- * Runtime-string-safe alignment lookup for the fixed-width control column.
49762
- *
49763
- * A device's `type` crosses the wire as a plain string;
49764
- * `DEVICE_TYPE_VALUES.has(type)` narrows it to `DeviceType` before indexing
49765
- * `CONTROL_FILLS`, making the cast sound. The `'table'` placement is
49766
- * content-width for EVERY type (returns `FIT`), so a wide control never
49767
- * overflows a fixed box — the FILL flag is irrelevant there. For the fixed-box
49768
- * placements a FILL type always stretches (`FILL`) and a natural-width type is
49769
- * placed by the caller's `placement` — `'center'` → `CENTER`, `'right'` →
49770
- * `RIGHT` (card / minimal view). An unrecognised type falls back to
49771
- * natural-width placement.
49772
- */
49773
- function resolveControlAlign(type, placement) {
49774
- if (placement === "table") return "fit";
49775
- return DEVICE_TYPE_VALUES$1.has(type) && CONTROL_FILLS[type] ? FILL : placement === "center" ? CENTER : RIGHT;
49776
- }
49777
- //#endregion
49778
- //#region src/lib/format-last-seen.ts
49779
- /**
49780
- * Relative "last seen" formatter for the offline affordance.
49781
- *
49782
- * Renders a short relative-time label from a millisecond epoch:
49783
- * "just now", "5m ago", "3h ago", "2d ago". Returns `null` for missing
49784
- * inputs, or inputs below {@link MIN_PLAUSIBLE_TIMESTAMP_MS} (a
49785
- * placeholder or a seeded "known value, unknown change time" stamp), so
49786
- * callers can render nothing rather than a misleading "—", "NaN ago", or
49787
- * (for a seeded `1`) "20000 days ago".
49788
- *
49789
- * Future timestamps (clock skew between the device and the browser)
49790
- * collapse to "just now" instead of a negative duration.
49791
- */
49792
- var SECOND_MS = 1e3;
49793
- var MINUTE_MS = 60 * SECOND_MS;
49794
- var HOUR_MS = 60 * MINUTE_MS;
49795
- var DAY_MS = 24 * HOUR_MS;
49796
- var JUST_NOW_MS = 30 * SECOND_MS;
49797
- /**
49798
- * The earliest timestamp a real "last seen" could plausibly carry. Below
49799
- * this, a value is a placeholder or a seeded "known value, unknown change
49800
- * time" stamp (e.g. Homematic's `HM_SEEDED_CHANGED_AT = 1`) — never a real
49801
- * instant. Year 2000 predates every device this renders.
49802
- */
49803
- var MIN_PLAUSIBLE_TIMESTAMP_MS = Date.UTC(2e3, 0, 1);
49804
- function formatLastSeen(timestampMs) {
49805
- if (timestampMs === null || timestampMs === void 0 || timestampMs < MIN_PLAUSIBLE_TIMESTAMP_MS) return null;
49806
- const delta = Date.now() - timestampMs;
49807
- if (delta < JUST_NOW_MS) return "just now";
49808
- if (delta < HOUR_MS) return `${String(Math.floor(delta / MINUTE_MS))}m ago`;
49809
- if (delta < DAY_MS) return `${String(Math.floor(delta / HOUR_MS))}h ago`;
49810
- return `${String(Math.floor(delta / DAY_MS))}d ago`;
49811
- }
49812
- //#endregion
49813
- //#region src/composites/device-controls/offline-badge.tsx
49814
- function OfflineBadge({ lastChangedAt, className }) {
49815
- const lastSeen = formatLastSeen(lastChangedAt);
49816
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
49817
- title: lastSeen === null ? "Unavailable" : `Unavailable · last seen ${lastSeen}`,
49818
- className: cn("inline-flex items-center gap-1 rounded-full px-2 py-0.5", "bg-surface-hover text-foreground-subtle text-[10px] font-medium", "border border-border", className),
49819
- children: [
49820
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WifiOff, {
49821
- className: "h-3 w-3 flex-shrink-0",
49822
- "aria-hidden": "true"
49823
- }),
49824
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "Unavailable" }),
49825
- lastSeen !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
49826
- className: "text-foreground-subtle/70",
49827
- children: `· last seen ${lastSeen}`
49828
- }) : null
49829
- ]
49830
- });
49831
- }
49832
- //#endregion
49833
- //#region src/composites/device-controls/primary-child.ts
49834
- /**
49835
- * Which child a container is rendered AS — this surface's name for the shared
49836
- * rule.
49837
- *
49838
- * The list and the resolution used to live here, in a COPY that carried a
49839
- * comment asking the Home Assistant adoption mapping and the viewer's panel to
49840
- * stay in sync with it. Three lists and three comments is not one rule, it is
49841
- * three chances to drift. Both now come from `@camstack/types`, which all of
49842
- * them already depend on; what remains here is the adapter for this module's
49843
- * historical `ChildRef` shape.
49844
- */
49845
- /** The shared precedence, under this module's historical name. */
49846
- var PRIORITY = _camstack_types.CONTAINER_CHILD_PRIORITY;
49847
- /**
49848
- * Resolve a container's primary child: the operator's pick when it still
49849
- * matches a live child, else the highest-priority type.
49850
- *
49851
- * `entityId` here is `sourceInfo?.id ?? stableId` (see `containerChildToRef`).
49852
- * The shared resolver matches its canonical `stableId` key first and accepts
49853
- * that legacy value too, so a pick stored under either keeps resolving.
49854
- */
49855
- function resolvePrimaryChild(children, overrideEntityId, containerName) {
49856
- const picked = (0, _camstack_types.resolveContainerPrimaryChild)(children.map((c) => ({
49857
- id: c.id,
49858
- stableId: c.entityId,
49859
- type: c.type,
49860
- ...c.role !== void 0 ? { role: c.role } : {},
49861
- ...c.name !== void 0 ? { name: c.name } : {}
49862
- })), overrideEntityId, containerName);
49863
- return picked === null ? null : children.find((c) => c.id === picked.id) ?? null;
49864
- }
49865
- //#endregion
49866
- //#region src/composites/device-controls/container-primary.ts
49867
- /**
49868
- * Re-sync-stable identity of a child. Prefers the upstream-system dispatch key
49869
- * (`sourceInfo.id` — e.g. HA `entity_id`); falls back to `stableId` for
49870
- * providers that never populated SourceInfo. Both survive a re-sync that may
49871
- * reallocate the numeric `id`.
49872
- */
49873
- function childEntityId(child) {
49874
- return child.sourceInfo?.id ?? child.stableId;
49875
- }
49876
- /** Project a raw child row onto the pure resolver's {@link ChildRef} shape. */
49877
- function containerChildToRef(child) {
49878
- return {
49879
- id: child.id,
49880
- entityId: childEntityId(child),
49881
- type: child.type,
49882
- ...child.role !== void 0 && child.role !== null ? { role: child.role } : {},
49883
- ...child.name !== void 0 ? { name: child.name } : {}
49884
- };
49885
- }
49886
- /**
49887
- * Resolve the entityId override from the container's persisted `linkDeviceId`.
49888
- * Returns the linked child's stable entityId, or `undefined` when there is no
49889
- * link or the link is stale (points to a numeric id no longer present) — in
49890
- * which case the resolver falls back to the priority default.
49891
- */
49892
- function overrideEntityIdFromLink(children, linkDeviceId) {
49893
- if (linkDeviceId === null || linkDeviceId === void 0) return void 0;
49894
- const linked = children.find((c) => c.id === linkDeviceId);
49895
- return linked === void 0 ? void 0 : childEntityId(linked);
49896
- }
49897
- /**
49898
- * Full container → primary-child resolution: project children to refs, resolve
49899
- * the override entityId, and delegate to the pure resolver. Returns `null` for a
49900
- * container with no children (caller degrades gracefully).
49901
- *
49902
- * Override precedence:
49903
- * 1. `primaryChildEntityId` — the DURABLE override (re-sync/rename-stable),
49904
- * passed straight to the resolver which matches on `entityId`. Wins when a
49905
- * non-empty string is supplied.
49906
- * 2. `linkDeviceId` — LEGACY numeric override, re-derived to an entityId from
49907
- * the live child set. Consulted only when there is no durable override.
49908
- *
49909
- * When neither resolves to a live child, the resolver falls back to the
49910
- * priority default.
49911
- */
49912
- function resolveContainerPrimary(children, linkDeviceId, primaryChildEntityId) {
49913
- return resolvePrimaryChild(children.map(containerChildToRef), primaryChildEntityId !== null && primaryChildEntityId !== void 0 && primaryChildEntityId !== "" ? primaryChildEntityId : overrideEntityIdFromLink(children, linkDeviceId));
49914
- }
49915
- //#endregion
49916
- //#region src/composites/device-controls/container-primary-hero.tsx
49917
- function ContainerPrimaryHero({ trpc, children, linkDeviceId, primaryChildEntityId }) {
49918
- const primary = resolveContainerPrimary(children, linkDeviceId, primaryChildEntityId);
49919
- const primaryChild = primary === null ? void 0 : children.find((c) => c.id === primary.id);
49920
- if (primaryChild === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
49921
- className: "flex h-full w-full items-center justify-center p-4 text-[11px] italic text-foreground-subtle",
49922
- children: "No child entities to display."
50706
+ function useLockCapGuard(trpc, deviceId) {
50707
+ const { cap } = useDeviceCapability({
50708
+ trpc,
50709
+ deviceId,
50710
+ cap: (d) => d.lockControl,
50711
+ slice: (d) => d.state.lockControl
49923
50712
  });
49924
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
49925
- className: "flex h-full w-full flex-col items-stretch gap-3 p-3 sm:p-4",
49926
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(resolveDeviceControl(primaryChild.type)?.HeroCard ?? DummyHeroCard, {
50713
+ return cap !== void 0;
50714
+ }
50715
+ function LockInlineControl({ trpc, deviceId, features, optimistic }) {
50716
+ if (!useLockCapGuard(trpc, deviceId)) return null;
50717
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LockPanel, {
50718
+ trpc,
50719
+ deviceId,
50720
+ features,
50721
+ layout: "compact",
50722
+ optimistic
50723
+ });
50724
+ }
50725
+ function LightInlineControl({ trpc, deviceId, features, optimistic }) {
50726
+ if (!useSwitchCapGuard(trpc, deviceId)) return null;
50727
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
50728
+ className: "inline-flex items-center gap-2",
50729
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SwitchPanel, {
49927
50730
  trpc,
49928
- deviceId: primaryChild.id,
49929
- features: primaryChild.features
49930
- })
50731
+ deviceId,
50732
+ features,
50733
+ layout: "compact",
50734
+ optimistic
50735
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BrightnessPanel, {
50736
+ trpc,
50737
+ deviceId,
50738
+ features,
50739
+ layout: "compact",
50740
+ optimistic
50741
+ })]
49931
50742
  });
49932
50743
  }
49933
- //#endregion
49934
- //#region src/composites/device-controls/primary-child-picker.tsx
49935
- var BASE_SELECT_CLASS = "rounded-md border border-border bg-surface px-1.5 py-0.5 text-[11px] text-foreground";
49936
- function PrimaryChildPicker({ childrenList, selectedId, onSetPrimary, disabled, className }) {
49937
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
49938
- value: selectedId,
49939
- disabled,
49940
- onChange: (e) => {
49941
- const nextId = Number(e.target.value);
49942
- if (!Number.isFinite(nextId) || nextId === selectedId) return;
49943
- const picked = childrenList.find((c) => c.id === nextId);
49944
- if (picked !== void 0) onSetPrimary(picked.stableId);
49945
- },
49946
- className: className === void 0 ? BASE_SELECT_CLASS : `${BASE_SELECT_CLASS} ${className}`,
49947
- children: childrenList.map((c) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
49948
- value: c.id,
49949
- children: [
49950
- c.name,
49951
- " (",
49952
- c.type,
49953
- ")"
49954
- ]
49955
- }, c.id))
50744
+ function useFanCapGuard(trpc, deviceId) {
50745
+ const { cap } = useDeviceCapability({
50746
+ trpc,
50747
+ deviceId,
50748
+ cap: (d) => d.fanControl,
50749
+ slice: (d) => d.state.fanControl
49956
50750
  });
50751
+ return cap !== void 0;
49957
50752
  }
49958
- //#endregion
49959
- //#region src/composites/device-list/batch-toolbar.tsx
49960
- function DeviceBatchToolbar({ selectedIds, onClear, remove, disable, enable }) {
49961
- const [pendingDelete, setPendingDelete] = (0, react$1.useState)(false);
49962
- if (selectedIds.size === 0) return null;
49963
- const ids = [...selectedIds];
49964
- const count = ids.length;
49965
- const handleDisable = () => {
49966
- for (const id of ids) disable({ deviceId: id });
49967
- onClear();
49968
- };
49969
- const handleEnable = () => {
49970
- for (const id of ids) enable({ deviceId: id });
49971
- onClear();
49972
- };
49973
- const handleDeleteFirst = () => {
49974
- setPendingDelete(true);
49975
- };
49976
- const handleDeleteConfirm = () => {
49977
- for (const id of ids) remove({ deviceId: id });
49978
- setPendingDelete(false);
49979
- onClear();
49980
- };
49981
- const handleDeleteCancel = () => {
49982
- setPendingDelete(false);
49983
- };
49984
- const handleClear = () => {
49985
- setPendingDelete(false);
49986
- onClear();
49987
- };
50753
+ function FanInlineControl({ trpc, deviceId, features, optimistic }) {
50754
+ if (!useFanCapGuard(trpc, deviceId)) return null;
49988
50755
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
49989
- className: "flex items-center gap-2 px-3 py-2 rounded-lg border border-border bg-surface-hover",
49990
- children: [
49991
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
49992
- className: "text-xs font-medium text-foreground-subtle shrink-0",
49993
- children: [count, " selected"]
49994
- }),
49995
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
49996
- className: "h-3.5 w-px bg-border shrink-0",
49997
- "aria-hidden": "true"
49998
- }),
49999
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
50000
- className: "flex items-center gap-1.5 flex-wrap",
50001
- children: [
50002
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
50003
- type: "button",
50004
- onClick: handleDisable,
50005
- className: cn("inline-flex items-center h-6 px-2.5 rounded-md border text-[10.5px] font-medium transition-colors", "border-border bg-surface text-foreground-subtle hover:text-foreground hover:bg-surface"),
50006
- children: "Disable"
50007
- }),
50008
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
50009
- type: "button",
50010
- onClick: handleEnable,
50011
- className: cn("inline-flex items-center h-6 px-2.5 rounded-md border text-[10.5px] font-medium transition-colors", "border-border bg-surface text-foreground-subtle hover:text-foreground hover:bg-surface"),
50012
- children: "Enable"
50013
- }),
50014
- pendingDelete ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
50015
- type: "button",
50016
- onClick: handleDeleteConfirm,
50017
- className: cn("inline-flex items-center h-6 px-2.5 rounded-md border text-[10.5px] font-medium transition-colors", "border-red-500/50 bg-red-500/10 text-red-400 hover:bg-red-500/20"),
50018
- children: [
50019
- "Confirm delete ",
50020
- count,
50021
- "?"
50022
- ]
50023
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
50024
- type: "button",
50025
- onClick: handleDeleteCancel,
50026
- className: cn("inline-flex items-center h-6 px-2.5 rounded-md border text-[10.5px] font-medium transition-colors", "border-border bg-surface text-foreground-subtle hover:text-foreground hover:bg-surface"),
50027
- children: "Cancel"
50028
- })] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
50029
- type: "button",
50030
- onClick: handleDeleteFirst,
50031
- className: cn("inline-flex items-center h-6 px-2.5 rounded-md border text-[10.5px] font-medium transition-colors", "border-red-500/40 bg-red-500/10 text-red-400 hover:bg-red-500/20"),
50032
- children: "Delete"
50033
- }),
50034
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
50035
- type: "button",
50036
- onClick: handleClear,
50037
- className: cn("inline-flex items-center h-6 px-2.5 rounded-md border text-[10.5px] font-medium transition-colors", "border-border bg-surface text-foreground-subtle hover:text-foreground hover:bg-surface"),
50038
- children: "Clear"
50039
- })
50040
- ]
50041
- })
50042
- ]
50756
+ className: "inline-flex items-center gap-2",
50757
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SwitchPanel, {
50758
+ trpc,
50759
+ deviceId,
50760
+ features,
50761
+ layout: "compact",
50762
+ optimistic
50763
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FanPanel, {
50764
+ trpc,
50765
+ deviceId,
50766
+ features,
50767
+ layout: "compact",
50768
+ optimistic
50769
+ })]
50043
50770
  });
50044
50771
  }
50045
- //#endregion
50046
- //#region src/composites/device-list/batch-actions-bar.tsx
50047
- /**
50048
- * <DeviceBatchActionsBar> — responsive wrapper around <DeviceBatchToolbar>.
50049
- *
50050
- * Surfaces the available bulk actions once devices are selected:
50051
- * - L2+ (`sm` and up): the toolbar renders inline as a row of buttons.
50052
- * - L1 (`< sm`): a compact "Actions · N" trigger opens a popover that
50053
- * hosts the SAME <DeviceBatchToolbar>, so the action logic (inline
50054
- * delete-confirm, clear) is reused verbatim — never forked.
50055
- *
50056
- * Renders nothing while the selection is empty (delegated to the toolbar).
50057
- */
50058
- function DeviceBatchActionsBar(props) {
50059
- const [open, setOpen] = (0, react$1.useState)(false);
50060
- const rootRef = (0, react$1.useRef)(null);
50061
- const count = props.selectedIds.size;
50062
- (0, react$1.useEffect)(() => {
50063
- if (!open) return;
50064
- const onOutside = (e) => {
50065
- const target = e.target instanceof Node ? e.target : null;
50066
- if (target && rootRef.current && !rootRef.current.contains(target)) setOpen(false);
50067
- };
50068
- const onKey = (e) => {
50069
- if (e.key === "Escape") setOpen(false);
50070
- };
50071
- document.addEventListener("mousedown", onOutside);
50072
- document.addEventListener("touchstart", onOutside);
50073
- document.addEventListener("keydown", onKey);
50074
- return () => {
50075
- document.removeEventListener("mousedown", onOutside);
50076
- document.removeEventListener("touchstart", onOutside);
50077
- document.removeEventListener("keydown", onKey);
50078
- };
50079
- }, [open]);
50080
- if (count === 0) return null;
50081
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
50082
- className: "hidden sm:block",
50083
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DeviceBatchToolbar, { ...props })
50084
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
50085
- ref: rootRef,
50086
- "data-testid": "batch-actions-compact",
50087
- className: "relative sm:hidden",
50088
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
50089
- type: "button",
50090
- onClick: () => setOpen((v) => !v),
50091
- "aria-haspopup": "menu",
50092
- "aria-expanded": open,
50093
- className: "inline-flex items-center gap-1.5 h-7 px-2.5 rounded-full border border-primary/40 bg-primary/15 text-[10.5px] font-medium text-primary",
50094
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: ["Actions · ", String(count)] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronDown, { className: `h-3 w-3 transition-transform ${open ? "rotate-180" : ""}` })]
50095
- }), open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
50096
- role: "menu",
50097
- className: "absolute left-0 top-full mt-1 z-30 rounded-lg border border-border bg-surface shadow-md",
50098
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DeviceBatchToolbar, { ...props })
50099
- })]
50100
- })] });
50772
+ function useClimateCapGuard(trpc, deviceId) {
50773
+ const { cap } = useDeviceCapability({
50774
+ trpc,
50775
+ deviceId,
50776
+ cap: (d) => d.climateControl,
50777
+ slice: (d) => d.state.climateControl
50778
+ });
50779
+ return cap !== void 0;
50101
50780
  }
50102
- //#endregion
50103
- //#region src/composites/device-meta.ts
50104
- var DEVICE_TYPE_META = {
50105
- [_camstack_types.DeviceType.Camera]: {
50106
- icon: Camera,
50107
- label: "Camera"
50108
- },
50109
- [_camstack_types.DeviceType.Hub]: {
50110
- icon: Network,
50111
- label: "Hub"
50112
- },
50113
- [_camstack_types.DeviceType.Light]: {
50114
- icon: Lightbulb,
50115
- label: "Light"
50116
- },
50117
- [_camstack_types.DeviceType.Siren]: {
50118
- icon: Siren,
50119
- label: "Siren"
50120
- },
50121
- [_camstack_types.DeviceType.Switch]: {
50122
- icon: ToggleLeft,
50123
- label: "Switch"
50124
- },
50125
- [_camstack_types.DeviceType.Sensor]: {
50126
- icon: Activity,
50127
- label: "Sensor"
50128
- },
50129
- [_camstack_types.DeviceType.Thermostat]: {
50130
- icon: Thermometer,
50131
- label: "Thermostat"
50132
- },
50133
- [_camstack_types.DeviceType.Climate]: {
50134
- icon: AirVent,
50135
- label: "Air conditioner"
50136
- },
50137
- [_camstack_types.DeviceType.Button]: {
50138
- icon: CircleDot,
50139
- label: "Button"
50140
- },
50141
- [_camstack_types.DeviceType.EventEmitter]: {
50142
- icon: Zap,
50143
- label: "Event Emitter"
50144
- },
50145
- [_camstack_types.DeviceType.Update]: {
50146
- icon: RefreshCw,
50147
- label: "Update"
50148
- },
50149
- [_camstack_types.DeviceType.Generic]: {
50150
- icon: Box,
50151
- label: "Generic"
50152
- },
50153
- [_camstack_types.DeviceType.Notifier]: {
50154
- icon: Bell,
50155
- label: "Notifier"
50156
- },
50157
- [_camstack_types.DeviceType.Script]: {
50158
- icon: ScrollText,
50159
- label: "Script"
50160
- },
50161
- [_camstack_types.DeviceType.Automation]: {
50162
- icon: Workflow,
50163
- label: "Automation"
50164
- },
50165
- [_camstack_types.DeviceType.Lock]: {
50166
- icon: Lock,
50167
- label: "Lock"
50168
- },
50169
- [_camstack_types.DeviceType.Cover]: {
50170
- icon: Blinds,
50171
- label: "Cover"
50172
- },
50173
- [_camstack_types.DeviceType.Valve]: {
50174
- icon: Pipette,
50175
- label: "Valve"
50176
- },
50177
- [_camstack_types.DeviceType.Humidifier]: {
50178
- icon: Waves,
50179
- label: "Humidifier"
50180
- },
50181
- [_camstack_types.DeviceType.WaterHeater]: {
50182
- icon: Bath,
50183
- label: "Water heater"
50184
- },
50185
- [_camstack_types.DeviceType.Fan]: {
50186
- icon: Fan,
50187
- label: "Fan"
50188
- },
50189
- [_camstack_types.DeviceType.MediaPlayer]: {
50190
- icon: MonitorPlay,
50191
- label: "Media player"
50192
- },
50193
- [_camstack_types.DeviceType.AlarmPanel]: {
50194
- icon: ShieldAlert,
50195
- label: "Alarm panel"
50196
- },
50197
- [_camstack_types.DeviceType.Control]: {
50198
- icon: SlidersHorizontal,
50199
- label: "Control"
50200
- },
50201
- [_camstack_types.DeviceType.Presence]: {
50202
- icon: MapPin,
50203
- label: "Presence"
50204
- },
50205
- [_camstack_types.DeviceType.Weather]: {
50206
- icon: CloudSun,
50207
- label: "Weather"
50208
- },
50209
- [_camstack_types.DeviceType.Vacuum]: {
50210
- icon: Bot,
50211
- label: "Vacuum"
50212
- },
50213
- [_camstack_types.DeviceType.LawnMower]: {
50214
- icon: Tractor,
50215
- label: "Lawn mower"
50216
- },
50217
- [_camstack_types.DeviceType.Container]: {
50218
- icon: Box,
50219
- label: "Container"
50220
- },
50221
- [_camstack_types.DeviceType.Image]: {
50222
- icon: Image,
50223
- label: "Image"
50224
- },
50225
- [_camstack_types.DeviceType.PetFeeder]: {
50226
- icon: Bone,
50227
- label: "Pet feeder"
50228
- }
50229
- };
50230
- var DEVICE_ROLE_META = {
50231
- [_camstack_types.DeviceRole.Siren]: {
50232
- icon: Siren,
50233
- label: "Siren"
50234
- },
50235
- [_camstack_types.DeviceRole.Floodlight]: {
50236
- icon: Lightbulb,
50237
- label: "Floodlight"
50238
- },
50239
- [_camstack_types.DeviceRole.Spotlight]: {
50240
- icon: Flashlight,
50241
- label: "Spotlight"
50242
- },
50243
- [_camstack_types.DeviceRole.PirSensor]: {
50244
- icon: Radar,
50245
- label: "PIR sensor"
50246
- },
50247
- [_camstack_types.DeviceRole.Chime]: {
50248
- icon: Bell,
50249
- label: "Chime"
50250
- },
50251
- [_camstack_types.DeviceRole.Autotrack]: {
50252
- icon: Crosshair,
50253
- label: "Auto-track"
50254
- },
50255
- [_camstack_types.DeviceRole.Nightvision]: {
50256
- icon: Moon,
50257
- label: "Night vision"
50258
- },
50259
- [_camstack_types.DeviceRole.PrivacyMask]: {
50260
- icon: EyeOff,
50261
- label: "Privacy mask"
50262
- },
50263
- [_camstack_types.DeviceRole.Doorbell]: {
50264
- icon: DoorClosed,
50265
- label: "Doorbell"
50266
- },
50267
- [_camstack_types.DeviceRole.BinaryHelper]: {
50268
- icon: ToggleLeft,
50269
- label: "Binary helper"
50270
- },
50271
- [_camstack_types.DeviceRole.MotionSensor]: {
50272
- icon: Footprints,
50273
- label: "Motion sensor"
50274
- },
50275
- [_camstack_types.DeviceRole.ContactSensor]: {
50276
- icon: DoorOpen,
50277
- label: "Contact sensor"
50278
- },
50279
- [_camstack_types.DeviceRole.LeakSensor]: {
50280
- icon: Droplet,
50281
- label: "Leak sensor"
50282
- },
50283
- [_camstack_types.DeviceRole.SmokeSensor]: {
50284
- icon: AlarmSmoke,
50285
- label: "Smoke sensor"
50286
- },
50287
- [_camstack_types.DeviceRole.COSensor]: {
50288
- icon: Wind,
50289
- label: "CO sensor"
50290
- },
50291
- [_camstack_types.DeviceRole.GasSensor]: {
50292
- icon: Flame,
50293
- label: "Gas sensor"
50294
- },
50295
- [_camstack_types.DeviceRole.TamperSensor]: {
50296
- icon: ShieldAlert,
50297
- label: "Tamper sensor"
50298
- },
50299
- [_camstack_types.DeviceRole.VibrationSensor]: {
50300
- icon: Vibrate,
50301
- label: "Vibration sensor"
50302
- },
50303
- [_camstack_types.DeviceRole.ConnectivitySensor]: {
50304
- icon: Wifi,
50305
- label: "Connectivity"
50306
- },
50307
- [_camstack_types.DeviceRole.SoundSensor]: {
50308
- icon: Volume2,
50309
- label: "Sound sensor"
50310
- },
50311
- [_camstack_types.DeviceRole.BinarySensor]: {
50312
- icon: ToggleLeft,
50313
- label: "Binary sensor"
50314
- },
50315
- [_camstack_types.DeviceRole.TemperatureSensor]: {
50316
- icon: Thermometer,
50317
- label: "Temperature"
50318
- },
50319
- [_camstack_types.DeviceRole.HumiditySensor]: {
50320
- icon: Droplets,
50321
- label: "Humidity"
50322
- },
50323
- [_camstack_types.DeviceRole.AmbientLightSensor]: {
50324
- icon: Sun,
50325
- label: "Ambient light"
50326
- },
50327
- [_camstack_types.DeviceRole.PressureSensor]: {
50328
- icon: Gauge,
50329
- label: "Pressure"
50781
+ function ThermostatInlineControl({ trpc, deviceId, features, optimistic }) {
50782
+ if (!useClimateCapGuard(trpc, deviceId)) return null;
50783
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ClimatePanel, {
50784
+ trpc,
50785
+ deviceId,
50786
+ features,
50787
+ layout: "compact",
50788
+ optimistic
50789
+ });
50790
+ }
50791
+ function AcInlineControl({ trpc, deviceId, features, optimistic }) {
50792
+ if (!useClimateCapGuard(trpc, deviceId)) return null;
50793
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AcPanel, {
50794
+ trpc,
50795
+ deviceId,
50796
+ features,
50797
+ layout: "compact",
50798
+ optimistic
50799
+ });
50800
+ }
50801
+ function AcHeroCard({ trpc, deviceId, features, optimistic }) {
50802
+ if (!useClimateCapGuard(trpc, deviceId)) return null;
50803
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AcPanel, {
50804
+ trpc,
50805
+ deviceId,
50806
+ features,
50807
+ layout: "full",
50808
+ optimistic
50809
+ });
50810
+ }
50811
+ function useMediaPlayerCapGuard(trpc, deviceId) {
50812
+ const { cap } = useDeviceCapability({
50813
+ trpc,
50814
+ deviceId,
50815
+ cap: (d) => d.mediaPlayer,
50816
+ slice: (d) => d.state.mediaPlayer
50817
+ });
50818
+ return cap !== void 0;
50819
+ }
50820
+ function MediaPlayerInlineControl({ trpc, deviceId, features, optimistic }) {
50821
+ if (!useMediaPlayerCapGuard(trpc, deviceId)) return null;
50822
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MediaPlayerPanel, {
50823
+ trpc,
50824
+ deviceId,
50825
+ features,
50826
+ layout: "compact",
50827
+ optimistic
50828
+ });
50829
+ }
50830
+ function useControlCapGuard(trpc, deviceId) {
50831
+ const { cap } = useDeviceCapability({
50832
+ trpc,
50833
+ deviceId,
50834
+ cap: (d) => d.control,
50835
+ slice: (d) => d.state.control
50836
+ });
50837
+ return cap !== void 0;
50838
+ }
50839
+ function ControlInlineControl({ trpc, deviceId, features, optimistic }) {
50840
+ if (!useControlCapGuard(trpc, deviceId)) return null;
50841
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ControlPanel, {
50842
+ trpc,
50843
+ deviceId,
50844
+ features,
50845
+ layout: "compact",
50846
+ optimistic
50847
+ });
50848
+ }
50849
+ function ControlHeroCard({ trpc, deviceId, features, optimistic }) {
50850
+ if (!useControlCapGuard(trpc, deviceId)) return null;
50851
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HeroShell, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ControlPanel, {
50852
+ trpc,
50853
+ deviceId,
50854
+ features,
50855
+ layout: "full",
50856
+ optimistic
50857
+ }) });
50858
+ }
50859
+ //#endregion
50860
+ //#region src/composites/device-controls/control-registry.tsx
50861
+ /**
50862
+ * Presentation binding: the shared `DeviceControlKind` (decided in
50863
+ * `@camstack/types` `resolveDeviceControlKind`) → this platform's web
50864
+ * components. Exhaustive over `DeviceControlKind` — omitting a kind fails the
50865
+ * build here. The type→kind DECISION is single-sourced in `@camstack/types`;
50866
+ * this map only binds a kind to its web widgets.
50867
+ */
50868
+ var CONTROL_KIND_COMPONENTS = {
50869
+ cover: {
50870
+ InlineControl: CoverInlineControl,
50871
+ HeroCard: CoverHeroCard
50330
50872
  },
50331
- [_camstack_types.DeviceRole.WindSensor]: {
50332
- icon: Wind,
50333
- label: "Wind"
50873
+ valve: {
50874
+ InlineControl: ValveInlineControl,
50875
+ HeroCard: ValveHeroCard
50334
50876
  },
50335
- [_camstack_types.DeviceRole.RainSensor]: {
50336
- icon: CloudRain,
50337
- label: "Rain"
50877
+ humidifier: {
50878
+ InlineControl: HumidifierInlineControl,
50879
+ HeroCard: HumidifierHeroCard
50338
50880
  },
50339
- [_camstack_types.DeviceRole.UvSensor]: {
50340
- icon: SunMedium,
50341
- label: "UV index"
50881
+ "water-heater": {
50882
+ InlineControl: WaterHeaterInlineControl,
50883
+ HeroCard: WaterHeaterHeroCard
50342
50884
  },
50343
- [_camstack_types.DeviceRole.SolarRadiationSensor]: {
50344
- icon: Sunrise,
50345
- label: "Solar radiation"
50885
+ switch: {
50886
+ InlineControl: SwitchInlineControl,
50887
+ HeroCard: SwitchHeroCard
50346
50888
  },
50347
- [_camstack_types.DeviceRole.SoilMoistureSensor]: {
50348
- icon: Sprout,
50349
- label: "Soil moisture"
50889
+ light: {
50890
+ InlineControl: LightInlineControl,
50891
+ HeroCard: LightHeroCard
50350
50892
  },
50351
- [_camstack_types.DeviceRole.PowerSensor]: {
50352
- icon: Zap,
50353
- label: "Power"
50893
+ fan: {
50894
+ InlineControl: FanInlineControl,
50895
+ HeroCard: FanHeroCard
50354
50896
  },
50355
- [_camstack_types.DeviceRole.EnergySensor]: {
50356
- icon: Zap,
50357
- label: "Energy"
50897
+ sensor: {
50898
+ InlineControl: SensorInlineControl,
50899
+ HeroCard: SensorHeroCard
50358
50900
  },
50359
- [_camstack_types.DeviceRole.VoltageSensor]: {
50360
- icon: Zap,
50361
- label: "Voltage"
50901
+ thermostat: {
50902
+ InlineControl: ThermostatInlineControl,
50903
+ HeroCard: ThermostatHeroCard
50362
50904
  },
50363
- [_camstack_types.DeviceRole.CurrentSensor]: {
50364
- icon: Zap,
50365
- label: "Current"
50905
+ climate: {
50906
+ InlineControl: AcInlineControl,
50907
+ HeroCard: AcHeroCard
50366
50908
  },
50367
- [_camstack_types.DeviceRole.AirQualitySensor]: {
50368
- icon: Wind,
50369
- label: "Air quality"
50909
+ button: {
50910
+ InlineControl: ButtonControl,
50911
+ HeroCard: ButtonHeroCard
50370
50912
  },
50371
- [_camstack_types.DeviceRole.BatterySensor]: {
50372
- icon: Gauge,
50373
- label: "Battery"
50913
+ update: {
50914
+ InlineControl: UpdateInlineControl,
50915
+ HeroCard: UpdateHeroCard
50374
50916
  },
50375
- [_camstack_types.DeviceRole.NumericSensor]: {
50376
- icon: Hash,
50377
- label: "Numeric sensor"
50917
+ lock: {
50918
+ InlineControl: LockInlineControl,
50919
+ HeroCard: LockHeroCard
50378
50920
  },
50379
- [_camstack_types.DeviceRole.EnumSensor]: {
50380
- icon: List,
50381
- label: "Enum sensor"
50921
+ "media-player": {
50922
+ InlineControl: MediaPlayerInlineControl,
50923
+ HeroCard: MediaPlayerHeroCard
50382
50924
  },
50383
- [_camstack_types.DeviceRole.DateTimeSensor]: {
50384
- icon: Clock,
50385
- label: "Date / time"
50925
+ alarm: {
50926
+ InlineControl: AlarmInlineControl,
50927
+ HeroCard: AlarmHeroCard
50386
50928
  },
50387
- [_camstack_types.DeviceRole.GenericSensor]: {
50388
- icon: Gauge,
50389
- label: "Sensor"
50929
+ control: {
50930
+ InlineControl: ControlInlineControl,
50931
+ HeroCard: ControlHeroCard
50390
50932
  },
50391
- [_camstack_types.DeviceRole.NumericControl]: {
50392
- icon: SlidersHorizontal,
50393
- label: "Numeric control"
50933
+ weather: {
50934
+ InlineControl: WeatherInlineControl,
50935
+ HeroCard: WeatherHeroCard
50394
50936
  },
50395
- [_camstack_types.DeviceRole.SelectControl]: {
50396
- icon: ListChecks,
50397
- label: "Select control"
50937
+ vacuum: {
50938
+ InlineControl: VacuumInlineControl,
50939
+ HeroCard: VacuumHeroCard
50398
50940
  },
50399
- [_camstack_types.DeviceRole.TextControl]: {
50400
- icon: Type,
50401
- label: "Text control"
50941
+ "lawn-mower": {
50942
+ InlineControl: LawnMowerInlineControl,
50943
+ HeroCard: LawnMowerHeroCard
50402
50944
  },
50403
- [_camstack_types.DeviceRole.DateTimeControl]: {
50404
- icon: Clock,
50405
- label: "Date / time control"
50945
+ image: {
50946
+ InlineControl: ImageInlineControl,
50947
+ HeroCard: ImageHeroCard
50406
50948
  },
50407
- [_camstack_types.DeviceRole.MobilePushNotifier]: {
50408
- icon: Smartphone,
50409
- label: "Mobile push"
50949
+ "pet-feeder": {
50950
+ InlineControl: PetFeederInlineControl,
50951
+ HeroCard: PetFeederHeroCard
50410
50952
  },
50411
- [_camstack_types.DeviceRole.MessagingNotifier]: {
50412
- icon: MessageSquare,
50413
- label: "Messaging"
50953
+ script: {
50954
+ InlineControl: ScriptInlineControl,
50955
+ HeroCard: ScriptHeroCard
50414
50956
  },
50415
- [_camstack_types.DeviceRole.EmailNotifier]: {
50416
- icon: Mail,
50417
- label: "Email"
50957
+ automation: {
50958
+ InlineControl: AutomationInlineControl,
50959
+ HeroCard: AutomationHeroCard
50418
50960
  },
50419
- [_camstack_types.DeviceRole.GenericNotifier]: {
50420
- icon: Bell,
50421
- label: "Notifier"
50961
+ dummy: {
50962
+ InlineControl: DummyInline,
50963
+ HeroCard: DummyHeroCard
50422
50964
  }
50423
50965
  };
50966
+ /**
50967
+ * Exhaustive record over DeviceType, DERIVED from the shared decision
50968
+ * (`DEVICE_TYPE_CONTROL_KIND`) composed with the web `CONTROL_KIND_COMPONENTS`
50969
+ * binding — so the type→control mapping can never diverge from the single
50970
+ * source in `@camstack/types`. Preserved as a public export for consumers/tests
50971
+ * that index it by `DeviceType`.
50972
+ *
50973
+ * The `Object.fromEntries` result is a `{ [k: string]: DeviceTypeControl }`;
50974
+ * `Object.values(DeviceType)` (a string enum) yields exactly the enum's members,
50975
+ * so the record is fully populated — the assertion narrows that documented
50976
+ * enum-record projection boundary.
50977
+ */
50978
+ var DEVICE_TYPE_CONTROL = Object.fromEntries(Object.values(_camstack_types.DeviceType).map((type) => [type, CONTROL_KIND_COMPONENTS[_camstack_types.DEVICE_TYPE_CONTROL_KIND[type]]]));
50979
+ /**
50980
+ * Per-type FILL flag.
50981
+ *
50982
+ * true — range / choice / button controls (cover ▲■▼ triad, thermostat
50983
+ * setpoint stepper, media/control/button action buttons, alarm
50984
+ * segmented control). The child STRETCHES to the full column width.
50985
+ * false — readouts, tap-toggles, icon controls (lock), and dummy/placeholder
50986
+ * types. The child keeps its NATURAL width; where it sits in the
50987
+ * column (flush right vs centered) is decided by the caller via a
50988
+ * `ControlPlacement` — see `resolveControlAlign`.
50989
+ *
50990
+ * Dummy types (Camera, Hub, Generic, Weather, Vacuum, Container, and placeholder
50991
+ * types Notifier/Script/Automation whose InlineControl renders "—") are `false` —
50992
+ * the dash placeholder must not be stretched to the full column width. (AlarmPanel
50993
+ * has a real segmented inline control and is `true`.) When a later task gives a
50994
+ * remaining placeholder type a real stretchy inline control, flip its flag to true.
50995
+ *
50996
+ * Exhaustive over DeviceType — a missing member is a TypeScript build error.
50997
+ */
50998
+ var CONTROL_FILLS = {
50999
+ [_camstack_types.DeviceType.Cover]: true,
51000
+ [_camstack_types.DeviceType.Valve]: true,
51001
+ [_camstack_types.DeviceType.Thermostat]: true,
51002
+ [_camstack_types.DeviceType.Climate]: true,
51003
+ [_camstack_types.DeviceType.Control]: true,
51004
+ [_camstack_types.DeviceType.Button]: true,
51005
+ [_camstack_types.DeviceType.MediaPlayer]: true,
51006
+ [_camstack_types.DeviceType.AlarmPanel]: true,
51007
+ [_camstack_types.DeviceType.Lock]: false,
51008
+ [_camstack_types.DeviceType.Vacuum]: false,
51009
+ [_camstack_types.DeviceType.LawnMower]: false,
51010
+ [_camstack_types.DeviceType.Humidifier]: false,
51011
+ [_camstack_types.DeviceType.Switch]: false,
51012
+ [_camstack_types.DeviceType.Siren]: false,
51013
+ [_camstack_types.DeviceType.Light]: false,
51014
+ [_camstack_types.DeviceType.Fan]: false,
51015
+ [_camstack_types.DeviceType.WaterHeater]: false,
51016
+ [_camstack_types.DeviceType.Sensor]: false,
51017
+ [_camstack_types.DeviceType.Presence]: false,
51018
+ [_camstack_types.DeviceType.EventEmitter]: false,
51019
+ [_camstack_types.DeviceType.Notifier]: false,
51020
+ [_camstack_types.DeviceType.Script]: false,
51021
+ [_camstack_types.DeviceType.Automation]: false,
51022
+ [_camstack_types.DeviceType.Camera]: false,
51023
+ [_camstack_types.DeviceType.Hub]: false,
51024
+ [_camstack_types.DeviceType.Update]: false,
51025
+ [_camstack_types.DeviceType.Generic]: false,
51026
+ [_camstack_types.DeviceType.Weather]: false,
51027
+ [_camstack_types.DeviceType.Container]: false,
51028
+ [_camstack_types.DeviceType.Image]: false,
51029
+ [_camstack_types.DeviceType.PetFeeder]: false
51030
+ };
50424
51031
  var DEVICE_TYPE_VALUES = new Set(Object.values(_camstack_types.DeviceType));
50425
- var DEVICE_ROLE_VALUES = new Set(Object.values(_camstack_types.DeviceRole));
50426
- function isDeviceType(value) {
50427
- return DEVICE_TYPE_VALUES.has(value);
50428
- }
50429
- function isDeviceRole(value) {
50430
- return DEVICE_ROLE_VALUES.has(value);
50431
- }
50432
- /** Meta for a known `DeviceType`. */
50433
- function deviceTypeMeta(type) {
50434
- return DEVICE_TYPE_META[type];
50435
- }
50436
- /** Meta for a known `DeviceRole`. */
50437
- function deviceRoleMeta(role) {
50438
- return DEVICE_ROLE_META[role];
51032
+ /**
51033
+ * Runtime-string-safe lookup: a device row's `type` crosses the wire as a plain
51034
+ * string. The type→control-kind DECISION is single-sourced in `@camstack/types`
51035
+ * (`resolveDeviceControlKind`, null for an unrecognised type); this maps the
51036
+ * resolved kind onto the web components.
51037
+ */
51038
+ function resolveDeviceControl(type) {
51039
+ const kind = (0, _camstack_types.resolveDeviceControlKind)(type);
51040
+ return kind ? CONTROL_KIND_COMPONENTS[kind] : null;
50439
51041
  }
50440
- var FALLBACK_DEVICE_TYPE_META = DEVICE_TYPE_META[_camstack_types.DeviceType.Generic];
50441
- var FALLBACK_DEVICE_ROLE_META = DEVICE_ROLE_META[_camstack_types.DeviceRole.GenericSensor];
50442
51042
  /**
50443
- * Runtime-string-safe lookup: device data crosses process / network boundaries
50444
- * as plain strings, so an unrecognised value falls back to the `Generic` meta
50445
- * instead of throwing.
51043
+ * Runtime-string-safe alignment lookup for the fixed-width control column.
50446
51044
  *
50447
- * This also guards mixed-version module-federation loads where the host
50448
- * `@camstack/types` enum has a new member before the loaded ui-library bundle
50449
- * has the matching `DEVICE_TYPE_META` entry.
51045
+ * A device's `type` crosses the wire as a plain string;
51046
+ * `DEVICE_TYPE_VALUES.has(type)` narrows it to `DeviceType` before indexing
51047
+ * `CONTROL_FILLS`, making the cast sound. The `'table'` placement is
51048
+ * content-width for EVERY type (returns `FIT`), so a wide control never
51049
+ * overflows a fixed box — the FILL flag is irrelevant there. For the fixed-box
51050
+ * placements a FILL type always stretches (`FILL`) and a natural-width type is
51051
+ * placed by the caller's `placement` — `'center'` → `CENTER`, `'right'` →
51052
+ * `RIGHT` (card / minimal view). An unrecognised type falls back to
51053
+ * natural-width placement.
50450
51054
  */
50451
- function deviceTypeMetaOf(type) {
50452
- if (!isDeviceType(type)) return FALLBACK_DEVICE_TYPE_META;
50453
- return DEVICE_TYPE_META[type] ?? FALLBACK_DEVICE_TYPE_META;
50454
- }
50455
- /** Runtime-string-safe role lookup; unknown roles fall back to `GenericSensor`. */
50456
- function deviceRoleMetaOf(role) {
50457
- if (!isDeviceRole(role)) return FALLBACK_DEVICE_ROLE_META;
50458
- return DEVICE_ROLE_META[role] ?? FALLBACK_DEVICE_ROLE_META;
51055
+ function resolveControlAlign(type, placement) {
51056
+ if (placement === "table") return "fit";
51057
+ return DEVICE_TYPE_VALUES.has(type) && CONTROL_FILLS[type] ? FILL : placement === "center" ? CENTER : RIGHT;
50459
51058
  }
51059
+ //#endregion
51060
+ //#region src/lib/format-last-seen.ts
50460
51061
  /**
50461
- * Icons an operator may pick for a per-device `display.icon` override (and for
50462
- * a per-role default), keyed by a stable kebab-case name. Named imports only —
50463
- * no dynamic indexing into lucide, so the bundle carries exactly this set.
50464
- * Unknown keys resolve to `undefined` (caller falls through to the next level).
51062
+ * Relative "last seen" formatter for the offline affordance.
50465
51063
  *
50466
- * The vocabulary is the one the VIEWER draws from too (its own icon module,
50467
- * keyed by these same strings): a key added here that the phone cannot draw
50468
- * falls through to the next resolution level there, which reads as "my change
50469
- * did nothing". So the KEY is the contract — a stable kebab-case string, never
50470
- * a glyph identity — and `device-meta.test.ts` pins that every key resolves
50471
- * and that the set never loses a key an operator may already have persisted.
51064
+ * Renders a short relative-time label from a millisecond epoch:
51065
+ * "just now", "5m ago", "3h ago", "2d ago". Returns `null` for missing
51066
+ * inputs, or inputs below {@link MIN_PLAUSIBLE_TIMESTAMP_MS} (a
51067
+ * placeholder or a seeded "known value, unknown change time" stamp), so
51068
+ * callers can render nothing rather than a misleading "—", "NaN ago", or
51069
+ * (for a seeded `1`) "20000 days ago".
50472
51070
  *
50473
- * Grouped by what an operator is naming: a device, a light or appliance, a
50474
- * sensor or the weather it reads, a room, or what moves through it. `gauge2`
50475
- * is a legacy alias kept for rows that persisted it.
51071
+ * Future timestamps (clock skew between the device and the browser)
51072
+ * collapse to "just now" instead of a negative duration.
50476
51073
  */
50477
- var DISPLAY_ICON_REGISTRY = {
50478
- camera: Camera,
50479
- network: Network,
50480
- "toggle-left": ToggleLeft,
50481
- "toggle-right": ToggleRight,
50482
- "circle-dot": CircleDot,
50483
- "refresh-cw": RefreshCw,
50484
- box: Box,
50485
- "scroll-text": ScrollText,
50486
- workflow: Workflow,
50487
- lock: Lock,
50488
- "lock-open": LockOpen,
50489
- blinds: Blinds,
50490
- "air-vent": AirVent,
50491
- "monitor-play": MonitorPlay,
50492
- "sliders-horizontal": SlidersHorizontal,
50493
- "map-pin": MapPin,
50494
- bot: Bot,
50495
- tractor: Tractor,
50496
- image: Image,
50497
- bone: Bone,
50498
- "door-closed": DoorClosed,
50499
- "door-open": DoorOpen,
50500
- crosshair: Crosshair,
50501
- eye: Eye,
50502
- "eye-off": EyeOff,
50503
- smartphone: Smartphone,
50504
- phone: Phone,
50505
- tablet: Tablet,
50506
- laptop: Laptop,
50507
- monitor: Monitor,
50508
- tv: Tv,
50509
- speaker: Speaker,
50510
- printer: Printer,
50511
- projector: Projector,
50512
- "gamepad-2": Gamepad2,
50513
- radio: Radio,
50514
- router: Router,
50515
- bluetooth: Bluetooth,
50516
- cpu: Cpu,
50517
- "hard-drive": HardDrive,
50518
- server: Server,
50519
- mic: Mic,
50520
- "mic-off": MicOff,
50521
- video: Video,
50522
- "video-off": VideoOff,
50523
- music: Music,
50524
- plug: Plug,
50525
- "plug-zap": PlugZap,
50526
- power: Power,
50527
- "power-off": PowerOff,
50528
- cable: Cable,
50529
- battery: Battery,
50530
- "battery-charging": BatteryCharging,
50531
- "battery-full": BatteryFull,
50532
- "battery-low": BatteryLow,
50533
- key: Key,
50534
- "key-round": KeyRound,
50535
- mailbox: Mailbox,
50536
- package: Package,
50537
- shield: Shield,
50538
- "shield-check": ShieldCheck,
50539
- "shield-off": ShieldOff,
50540
- lightbulb: Lightbulb,
50541
- lamp: Lamp,
50542
- "lamp-ceiling": LampCeiling,
50543
- "lamp-desk": LampDesk,
50544
- "lamp-floor": LampFloor,
50545
- flashlight: Flashlight,
50546
- heater: Heater,
50547
- snowflake: Snowflake,
50548
- "thermometer-sun": ThermometerSun,
50549
- "thermometer-snowflake": ThermometerSnowflake,
50550
- refrigerator: Refrigerator,
50551
- microwave: Microwave,
50552
- "washing-machine": WashingMachine,
50553
- "cooking-pot": CookingPot,
50554
- utensils: Utensils,
50555
- thermometer: Thermometer,
50556
- droplets: Droplets,
50557
- droplet: Droplet,
50558
- gauge: Gauge,
50559
- gauge2: Gauge,
50560
- sun: Sun,
50561
- "sun-medium": SunMedium,
50562
- sunrise: Sunrise,
50563
- moon: Moon,
50564
- wind: Wind,
50565
- cloud: Cloud,
50566
- "cloud-rain": CloudRain,
50567
- "cloud-sun": CloudSun,
50568
- waves: Waves,
50569
- sprout: Sprout,
50570
- leaf: Leaf,
50571
- flower: Flower,
50572
- zap: Zap,
50573
- flame: Flame,
50574
- bell: Bell,
50575
- "bell-ring": BellRing,
50576
- "bell-off": BellOff,
50577
- siren: Siren,
50578
- radar: Radar,
50579
- activity: Activity,
50580
- hash: Hash,
50581
- clock: Clock,
50582
- timer: Timer,
50583
- calendar: Calendar,
50584
- pipette: Pipette,
50585
- fan: Fan,
50586
- bath: Bath,
50587
- footprints: Footprints,
50588
- "alarm-smoke": AlarmSmoke,
50589
- "shield-alert": ShieldAlert,
50590
- vibrate: Vibrate,
50591
- wifi: Wifi,
50592
- "wifi-off": WifiOff,
50593
- "volume-2": Volume2,
50594
- "volume-x": VolumeX,
50595
- heart: Heart,
50596
- "heart-pulse": HeartPulse,
50597
- star: Star,
50598
- sparkles: Sparkles,
50599
- house: House,
50600
- sofa: Sofa,
50601
- "bed-double": BedDouble,
50602
- bed: Bed,
50603
- warehouse: Warehouse,
50604
- trees: Trees,
50605
- "tree-pine": TreePine,
50606
- fence: Fence,
50607
- store: Store,
50608
- building: Building,
50609
- "building-2": Building2,
50610
- mountain: Mountain,
50611
- umbrella: Umbrella,
50612
- dumbbell: Dumbbell,
50613
- wrench: Wrench,
50614
- hammer: Hammer,
50615
- drill: Drill,
50616
- "waves-ladder": WavesLadder,
50617
- "circle-parking": CircleParking,
50618
- briefcase: Briefcase,
50619
- route: Route,
50620
- car: Car,
50621
- bike: Bike,
50622
- bus: Bus,
50623
- truck: Truck,
50624
- plane: Plane,
50625
- "train-front": TrainFront,
50626
- sailboat: Sailboat,
50627
- dog: Dog,
50628
- cat: Cat,
50629
- bird: Bird,
50630
- "paw-print": PawPrint,
50631
- baby: Baby,
50632
- user: User,
50633
- users: Users,
50634
- "person-standing": PersonStanding
50635
- };
50636
- /** Resolve a `display.icon` name to a lucide component, or `undefined` when the
50637
- * name is not in the curated `DISPLAY_ICON_REGISTRY`. */
50638
- function resolveDisplayIcon(name) {
50639
- return Object.hasOwn(DISPLAY_ICON_REGISTRY, name) ? DISPLAY_ICON_REGISTRY[name] : void 0;
51074
+ var SECOND_MS = 1e3;
51075
+ var MINUTE_MS = 60 * SECOND_MS;
51076
+ var HOUR_MS = 60 * MINUTE_MS;
51077
+ var DAY_MS = 24 * HOUR_MS;
51078
+ var JUST_NOW_MS = 30 * SECOND_MS;
51079
+ /**
51080
+ * The earliest timestamp a real "last seen" could plausibly carry. Below
51081
+ * this, a value is a placeholder or a seeded "known value, unknown change
51082
+ * time" stamp (e.g. Homematic's `HM_SEEDED_CHANGED_AT = 1`) — never a real
51083
+ * instant. Year 2000 predates every device this renders.
51084
+ */
51085
+ var MIN_PLAUSIBLE_TIMESTAMP_MS = Date.UTC(2e3, 0, 1);
51086
+ function formatLastSeen(timestampMs) {
51087
+ if (timestampMs === null || timestampMs === void 0 || timestampMs < MIN_PLAUSIBLE_TIMESTAMP_MS) return null;
51088
+ const delta = Date.now() - timestampMs;
51089
+ if (delta < JUST_NOW_MS) return "just now";
51090
+ if (delta < HOUR_MS) return `${String(Math.floor(delta / MINUTE_MS))}m ago`;
51091
+ if (delta < DAY_MS) return `${String(Math.floor(delta / HOUR_MS))}h ago`;
51092
+ return `${String(Math.floor(delta / DAY_MS))}d ago`;
51093
+ }
51094
+ //#endregion
51095
+ //#region src/composites/device-controls/offline-badge.tsx
51096
+ function OfflineBadge({ lastChangedAt, className }) {
51097
+ const lastSeen = formatLastSeen(lastChangedAt);
51098
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
51099
+ title: lastSeen === null ? "Unavailable" : `Unavailable · last seen ${lastSeen}`,
51100
+ className: cn("inline-flex items-center gap-1 rounded-full px-2 py-0.5", "bg-surface-hover text-foreground-subtle text-[10px] font-medium", "border border-border", className),
51101
+ children: [
51102
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WifiOff, {
51103
+ className: "h-3 w-3 flex-shrink-0",
51104
+ "aria-hidden": "true"
51105
+ }),
51106
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "Unavailable" }),
51107
+ lastSeen !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
51108
+ className: "text-foreground-subtle/70",
51109
+ children: `· last seen ${lastSeen}`
51110
+ }) : null
51111
+ ]
51112
+ });
50640
51113
  }
51114
+ //#endregion
51115
+ //#region src/composites/device-controls/primary-child.ts
50641
51116
  /**
50642
- * EXHAUSTIVE device-type filter options — one entry per `DeviceType` member,
50643
- * derived from `DEVICE_TYPE_META` so the set is always complete and a newly
50644
- * added `DeviceType` automatically appears in every type filter (Devices page,
50645
- * adoption modal, …) without touching any consumer.
51117
+ * Which child a container is rendered AS — this surface's name for the shared
51118
+ * rule.
50646
51119
  *
50647
- * The previous per-consumer logic built options from the types PRESENT in the
50648
- * list (or a hand-maintained subset), so recently-added types (vacuum, lawn
50649
- * mower, valve, humidifier, water heater, weather, image, …) were silently
50650
- * missing from the dropdown. This is the single source of truth instead.
51120
+ * The list and the resolution used to live here, in a COPY that carried a
51121
+ * comment asking the Home Assistant adoption mapping and the viewer's panel to
51122
+ * stay in sync with it. Three lists and three comments is not one rule, it is
51123
+ * three chances to drift. Both now come from `@camstack/types`, which all of
51124
+ * them already depend on; what remains here is the adapter for this module's
51125
+ * historical `ChildRef` shape.
51126
+ */
51127
+ /** The shared precedence, under this module's historical name. */
51128
+ var PRIORITY = _camstack_types.CONTAINER_CHILD_PRIORITY;
51129
+ /**
51130
+ * Resolve a container's primary child: the operator's pick when it still
51131
+ * matches a live child, else the highest-priority type.
50651
51132
  *
50652
- * `id` is the raw `DeviceType` string (matches the filtering predicate, which
50653
- * compares against `device.type`); `label` + `icon` come from the meta map.
50654
- * Ordered by `DeviceType` declaration order for a stable, intentional layout.
51133
+ * `entityId` here is `sourceInfo?.id ?? stableId` (see `containerChildToRef`).
51134
+ * The shared resolver matches its canonical `stableId` key first and accepts
51135
+ * that legacy value too, so a pick stored under either keeps resolving.
50655
51136
  */
50656
- var ALL_DEVICE_TYPE_FILTER_OPTIONS = Object.values(_camstack_types.DeviceType).map((type) => {
50657
- const meta = deviceTypeMetaOf(type);
51137
+ function resolvePrimaryChild(children, overrideEntityId, containerName) {
51138
+ const picked = (0, _camstack_types.resolveContainerPrimaryChild)(children.map((c) => ({
51139
+ id: c.id,
51140
+ stableId: c.entityId,
51141
+ type: c.type,
51142
+ ...c.role !== void 0 ? { role: c.role } : {},
51143
+ ...c.name !== void 0 ? { name: c.name } : {}
51144
+ })), overrideEntityId, containerName);
51145
+ return picked === null ? null : children.find((c) => c.id === picked.id) ?? null;
51146
+ }
51147
+ //#endregion
51148
+ //#region src/composites/device-controls/container-primary.ts
51149
+ /**
51150
+ * Re-sync-stable identity of a child. Prefers the upstream-system dispatch key
51151
+ * (`sourceInfo.id` — e.g. HA `entity_id`); falls back to `stableId` for
51152
+ * providers that never populated SourceInfo. Both survive a re-sync that may
51153
+ * reallocate the numeric `id`.
51154
+ */
51155
+ function childEntityId(child) {
51156
+ return child.sourceInfo?.id ?? child.stableId;
51157
+ }
51158
+ /** Project a raw child row onto the pure resolver's {@link ChildRef} shape. */
51159
+ function containerChildToRef(child) {
50658
51160
  return {
50659
- id: type,
50660
- label: meta.label,
50661
- icon: meta.icon
51161
+ id: child.id,
51162
+ entityId: childEntityId(child),
51163
+ type: child.type,
51164
+ ...child.role !== void 0 && child.role !== null ? { role: child.role } : {},
51165
+ ...child.name !== void 0 ? { name: child.name } : {}
50662
51166
  };
50663
- });
50664
- function allDeviceTypeFilterOptions() {
50665
- return ALL_DEVICE_TYPE_FILTER_OPTIONS;
51167
+ }
51168
+ /**
51169
+ * Resolve the entityId override from the container's persisted `linkDeviceId`.
51170
+ * Returns the linked child's stable entityId, or `undefined` when there is no
51171
+ * link or the link is stale (points to a numeric id no longer present) — in
51172
+ * which case the resolver falls back to the priority default.
51173
+ */
51174
+ function overrideEntityIdFromLink(children, linkDeviceId) {
51175
+ if (linkDeviceId === null || linkDeviceId === void 0) return void 0;
51176
+ const linked = children.find((c) => c.id === linkDeviceId);
51177
+ return linked === void 0 ? void 0 : childEntityId(linked);
51178
+ }
51179
+ /**
51180
+ * Full container → primary-child resolution: project children to refs, resolve
51181
+ * the override entityId, and delegate to the pure resolver. Returns `null` for a
51182
+ * container with no children (caller degrades gracefully).
51183
+ *
51184
+ * Override precedence:
51185
+ * 1. `primaryChildEntityId` — the DURABLE override (re-sync/rename-stable),
51186
+ * passed straight to the resolver which matches on `entityId`. Wins when a
51187
+ * non-empty string is supplied.
51188
+ * 2. `linkDeviceId` — LEGACY numeric override, re-derived to an entityId from
51189
+ * the live child set. Consulted only when there is no durable override.
51190
+ *
51191
+ * When neither resolves to a live child, the resolver falls back to the
51192
+ * priority default.
51193
+ */
51194
+ function resolveContainerPrimary(children, linkDeviceId, primaryChildEntityId) {
51195
+ return resolvePrimaryChild(children.map(containerChildToRef), primaryChildEntityId !== null && primaryChildEntityId !== void 0 && primaryChildEntityId !== "" ? primaryChildEntityId : overrideEntityIdFromLink(children, linkDeviceId));
51196
+ }
51197
+ //#endregion
51198
+ //#region src/composites/device-controls/container-primary-hero.tsx
51199
+ function ContainerPrimaryHero({ trpc, children, linkDeviceId, primaryChildEntityId }) {
51200
+ const primary = resolveContainerPrimary(children, linkDeviceId, primaryChildEntityId);
51201
+ const primaryChild = primary === null ? void 0 : children.find((c) => c.id === primary.id);
51202
+ if (primaryChild === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
51203
+ className: "flex h-full w-full items-center justify-center p-4 text-[11px] italic text-foreground-subtle",
51204
+ children: "No child entities to display."
51205
+ });
51206
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
51207
+ className: "flex h-full w-full flex-col items-stretch gap-3 p-3 sm:p-4",
51208
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(resolveDeviceControl(primaryChild.type)?.HeroCard ?? DummyHeroCard, {
51209
+ trpc,
51210
+ deviceId: primaryChild.id,
51211
+ features: primaryChild.features
51212
+ })
51213
+ });
51214
+ }
51215
+ //#endregion
51216
+ //#region src/composites/device-controls/primary-child-picker.tsx
51217
+ var BASE_SELECT_CLASS = "rounded-md border border-border bg-surface px-1.5 py-0.5 text-[11px] text-foreground";
51218
+ function PrimaryChildPicker({ childrenList, selectedId, onSetPrimary, disabled, className }) {
51219
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
51220
+ value: selectedId,
51221
+ disabled,
51222
+ onChange: (e) => {
51223
+ const nextId = Number(e.target.value);
51224
+ if (!Number.isFinite(nextId) || nextId === selectedId) return;
51225
+ const picked = childrenList.find((c) => c.id === nextId);
51226
+ if (picked !== void 0) onSetPrimary(picked.stableId);
51227
+ },
51228
+ className: className === void 0 ? BASE_SELECT_CLASS : `${BASE_SELECT_CLASS} ${className}`,
51229
+ children: childrenList.map((c) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
51230
+ value: c.id,
51231
+ children: [
51232
+ c.name,
51233
+ " (",
51234
+ c.type,
51235
+ ")"
51236
+ ]
51237
+ }, c.id))
51238
+ });
51239
+ }
51240
+ //#endregion
51241
+ //#region src/composites/device-list/batch-toolbar.tsx
51242
+ function DeviceBatchToolbar({ selectedIds, onClear, remove, disable, enable }) {
51243
+ const [pendingDelete, setPendingDelete] = (0, react$1.useState)(false);
51244
+ if (selectedIds.size === 0) return null;
51245
+ const ids = [...selectedIds];
51246
+ const count = ids.length;
51247
+ const handleDisable = () => {
51248
+ for (const id of ids) disable({ deviceId: id });
51249
+ onClear();
51250
+ };
51251
+ const handleEnable = () => {
51252
+ for (const id of ids) enable({ deviceId: id });
51253
+ onClear();
51254
+ };
51255
+ const handleDeleteFirst = () => {
51256
+ setPendingDelete(true);
51257
+ };
51258
+ const handleDeleteConfirm = () => {
51259
+ for (const id of ids) remove({ deviceId: id });
51260
+ setPendingDelete(false);
51261
+ onClear();
51262
+ };
51263
+ const handleDeleteCancel = () => {
51264
+ setPendingDelete(false);
51265
+ };
51266
+ const handleClear = () => {
51267
+ setPendingDelete(false);
51268
+ onClear();
51269
+ };
51270
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
51271
+ className: "flex items-center gap-2 px-3 py-2 rounded-lg border border-border bg-surface-hover",
51272
+ children: [
51273
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
51274
+ className: "text-xs font-medium text-foreground-subtle shrink-0",
51275
+ children: [count, " selected"]
51276
+ }),
51277
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
51278
+ className: "h-3.5 w-px bg-border shrink-0",
51279
+ "aria-hidden": "true"
51280
+ }),
51281
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
51282
+ className: "flex items-center gap-1.5 flex-wrap",
51283
+ children: [
51284
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
51285
+ type: "button",
51286
+ onClick: handleDisable,
51287
+ className: cn("inline-flex items-center h-6 px-2.5 rounded-md border text-[10.5px] font-medium transition-colors", "border-border bg-surface text-foreground-subtle hover:text-foreground hover:bg-surface"),
51288
+ children: "Disable"
51289
+ }),
51290
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
51291
+ type: "button",
51292
+ onClick: handleEnable,
51293
+ className: cn("inline-flex items-center h-6 px-2.5 rounded-md border text-[10.5px] font-medium transition-colors", "border-border bg-surface text-foreground-subtle hover:text-foreground hover:bg-surface"),
51294
+ children: "Enable"
51295
+ }),
51296
+ pendingDelete ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
51297
+ type: "button",
51298
+ onClick: handleDeleteConfirm,
51299
+ className: cn("inline-flex items-center h-6 px-2.5 rounded-md border text-[10.5px] font-medium transition-colors", "border-red-500/50 bg-red-500/10 text-red-400 hover:bg-red-500/20"),
51300
+ children: [
51301
+ "Confirm delete ",
51302
+ count,
51303
+ "?"
51304
+ ]
51305
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
51306
+ type: "button",
51307
+ onClick: handleDeleteCancel,
51308
+ className: cn("inline-flex items-center h-6 px-2.5 rounded-md border text-[10.5px] font-medium transition-colors", "border-border bg-surface text-foreground-subtle hover:text-foreground hover:bg-surface"),
51309
+ children: "Cancel"
51310
+ })] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
51311
+ type: "button",
51312
+ onClick: handleDeleteFirst,
51313
+ className: cn("inline-flex items-center h-6 px-2.5 rounded-md border text-[10.5px] font-medium transition-colors", "border-red-500/40 bg-red-500/10 text-red-400 hover:bg-red-500/20"),
51314
+ children: "Delete"
51315
+ }),
51316
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
51317
+ type: "button",
51318
+ onClick: handleClear,
51319
+ className: cn("inline-flex items-center h-6 px-2.5 rounded-md border text-[10.5px] font-medium transition-colors", "border-border bg-surface text-foreground-subtle hover:text-foreground hover:bg-surface"),
51320
+ children: "Clear"
51321
+ })
51322
+ ]
51323
+ })
51324
+ ]
51325
+ });
51326
+ }
51327
+ //#endregion
51328
+ //#region src/composites/device-list/batch-actions-bar.tsx
51329
+ /**
51330
+ * <DeviceBatchActionsBar> — responsive wrapper around <DeviceBatchToolbar>.
51331
+ *
51332
+ * Surfaces the available bulk actions once devices are selected:
51333
+ * - L2+ (`sm` and up): the toolbar renders inline as a row of buttons.
51334
+ * - L1 (`< sm`): a compact "Actions · N" trigger opens a popover that
51335
+ * hosts the SAME <DeviceBatchToolbar>, so the action logic (inline
51336
+ * delete-confirm, clear) is reused verbatim — never forked.
51337
+ *
51338
+ * Renders nothing while the selection is empty (delegated to the toolbar).
51339
+ */
51340
+ function DeviceBatchActionsBar(props) {
51341
+ const [open, setOpen] = (0, react$1.useState)(false);
51342
+ const rootRef = (0, react$1.useRef)(null);
51343
+ const count = props.selectedIds.size;
51344
+ (0, react$1.useEffect)(() => {
51345
+ if (!open) return;
51346
+ const onOutside = (e) => {
51347
+ const target = e.target instanceof Node ? e.target : null;
51348
+ if (target && rootRef.current && !rootRef.current.contains(target)) setOpen(false);
51349
+ };
51350
+ const onKey = (e) => {
51351
+ if (e.key === "Escape") setOpen(false);
51352
+ };
51353
+ document.addEventListener("mousedown", onOutside);
51354
+ document.addEventListener("touchstart", onOutside);
51355
+ document.addEventListener("keydown", onKey);
51356
+ return () => {
51357
+ document.removeEventListener("mousedown", onOutside);
51358
+ document.removeEventListener("touchstart", onOutside);
51359
+ document.removeEventListener("keydown", onKey);
51360
+ };
51361
+ }, [open]);
51362
+ if (count === 0) return null;
51363
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
51364
+ className: "hidden sm:block",
51365
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DeviceBatchToolbar, { ...props })
51366
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
51367
+ ref: rootRef,
51368
+ "data-testid": "batch-actions-compact",
51369
+ className: "relative sm:hidden",
51370
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
51371
+ type: "button",
51372
+ onClick: () => setOpen((v) => !v),
51373
+ "aria-haspopup": "menu",
51374
+ "aria-expanded": open,
51375
+ className: "inline-flex items-center gap-1.5 h-7 px-2.5 rounded-full border border-primary/40 bg-primary/15 text-[10.5px] font-medium text-primary",
51376
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: ["Actions · ", String(count)] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronDown, { className: `h-3 w-3 transition-transform ${open ? "rotate-180" : ""}` })]
51377
+ }), open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
51378
+ role: "menu",
51379
+ className: "absolute left-0 top-full mt-1 z-30 rounded-lg border border-border bg-surface shadow-md",
51380
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DeviceBatchToolbar, { ...props })
51381
+ })]
51382
+ })] });
50666
51383
  }
50667
51384
  //#endregion
50668
51385
  //#region src/hooks/use-debounced-string.ts
@@ -50683,6 +51400,59 @@ function useDebouncedString(value, delayMs) {
50683
51400
  return debounced;
50684
51401
  }
50685
51402
  //#endregion
51403
+ //#region src/hooks/use-persisted-page-size.ts
51404
+ /**
51405
+ * usePersistedPageSize — a "rows per page" choice remembered per viewer.
51406
+ *
51407
+ * The caller names the storage key, the allowed sizes and the default. Each
51408
+ * surface with its own default or option set keeps its own key, so one
51409
+ * surface's choice never lands on another that cannot show it (the device
51410
+ * list's global key stays `useDeviceListPageSize`'s, with its own options).
51411
+ *
51412
+ * `PAGE_SIZE_ALL` (0) is the "All" option: one page holding every row.
51413
+ *
51414
+ * Storage is a convenience, never a requirement: every read and write is
51415
+ * guarded, and a blocked / private-mode / missing storage simply means the
51416
+ * choice lasts until the page is left. A stored value that is not one of the
51417
+ * options (an older option set, a hand-edited value) is ignored.
51418
+ */
51419
+ /** The "All" option — one page holding every row. */
51420
+ var PAGE_SIZE_ALL = 0;
51421
+ function readStored(key, options, fallback) {
51422
+ if (key === null || typeof window === "undefined") return fallback;
51423
+ try {
51424
+ const raw = window.localStorage.getItem(key);
51425
+ if (raw === null) return fallback;
51426
+ const parsed = Number.parseInt(raw, 10);
51427
+ return Number.isFinite(parsed) && options.includes(parsed) ? parsed : fallback;
51428
+ } catch {
51429
+ return fallback;
51430
+ }
51431
+ }
51432
+ function writeStored(key, value) {
51433
+ if (key === null || typeof window === "undefined") return;
51434
+ try {
51435
+ window.localStorage.setItem(key, String(value));
51436
+ } catch {}
51437
+ }
51438
+ /** @returns `[size, setSize]`; `setSize` ignores a size that is not an option. */
51439
+ function usePersistedPageSize({ storageKey, options, defaultSize }) {
51440
+ const seed = options.includes(defaultSize) ? defaultSize : options[0] ?? defaultSize;
51441
+ const [size, setSize] = (0, react$1.useState)(() => readStored(storageKey, options, seed));
51442
+ return [size, (0, react$1.useCallback)((next) => {
51443
+ if (!options.includes(next)) return;
51444
+ setSize(next);
51445
+ writeStored(storageKey, next);
51446
+ }, [options, storageKey])];
51447
+ }
51448
+ /**
51449
+ * The number of rows one page holds for a chosen size: "All" is every row
51450
+ * (at least 1, so the pager arithmetic never divides by zero).
51451
+ */
51452
+ function effectivePageSize(size, totalItems) {
51453
+ return size === 0 ? Math.max(1, totalItems) : size;
51454
+ }
51455
+ //#endregion
50686
51456
  //#region src/hooks/use-device-list-page-size.ts
50687
51457
  /**
50688
51458
  * useDeviceListPageSize — global page-size preference for the device-list
@@ -50693,8 +51463,9 @@ function useDebouncedString(value, delayMs) {
50693
51463
  * value is validated against {@link DEVICE_LIST_PAGE_SIZE_OPTIONS}; any
50694
51464
  * garbage / out-of-range value falls back to the provided default.
50695
51465
  *
50696
- * SSR / jsdom-safe: every `window` access is guarded so the hook can run in
50697
- * a non-browser environment without throwing.
51466
+ * A thin binding of `usePersistedPageSize` to that key and option set; a
51467
+ * surface with a different default or options uses the generic hook with a
51468
+ * key of its own.
50698
51469
  */
50699
51470
  /** Selectable per-page sizes. The table default (50) is intentionally one of them. */
50700
51471
  var DEVICE_LIST_PAGE_SIZE_OPTIONS = [
@@ -50705,33 +51476,6 @@ var DEVICE_LIST_PAGE_SIZE_OPTIONS = [
50705
51476
  ];
50706
51477
  /** Single global key shared across every device-list surface. */
50707
51478
  var DEVICE_LIST_PAGE_SIZE_KEY = "camstack.device-list.pageSize";
50708
- function isAllowed(value) {
50709
- return DEVICE_LIST_PAGE_SIZE_OPTIONS.includes(value);
50710
- }
50711
- /**
50712
- * Normalize a caller-supplied `defaultSize` to an allowed option, falling back
50713
- * to the canonical default so a stray value can never seed a phantom size.
50714
- */
50715
- function safeDefault(defaultSize) {
50716
- return isAllowed(defaultSize) ? defaultSize : 50;
50717
- }
50718
- function readStored(defaultSize) {
50719
- if (typeof window === "undefined") return defaultSize;
50720
- try {
50721
- const raw = window.localStorage.getItem(DEVICE_LIST_PAGE_SIZE_KEY);
50722
- if (raw === null) return defaultSize;
50723
- const parsed = Number.parseInt(raw, 10);
50724
- return Number.isFinite(parsed) && isAllowed(parsed) ? parsed : defaultSize;
50725
- } catch {
50726
- return defaultSize;
50727
- }
50728
- }
50729
- function writeStored(value) {
50730
- if (typeof window === "undefined") return;
50731
- try {
50732
- window.localStorage.setItem(DEVICE_LIST_PAGE_SIZE_KEY, String(value));
50733
- } catch {}
50734
- }
50735
51479
  /**
50736
51480
  * @param defaultSize fallback when nothing valid is stored. If it is not itself
50737
51481
  * a member of {@link DEVICE_LIST_PAGE_SIZE_OPTIONS}, it is replaced by
@@ -50739,13 +51483,11 @@ function writeStored(value) {
50739
51483
  * @returns `[size, setSize]` — `setSize` ignores values outside the allowed set.
50740
51484
  */
50741
51485
  function useDeviceListPageSize(defaultSize) {
50742
- const seed = safeDefault(defaultSize);
50743
- const [size, setSize] = (0, react$1.useState)(() => readStored(seed));
50744
- return [size, (0, react$1.useCallback)((next) => {
50745
- if (!isAllowed(next)) return;
50746
- setSize(next);
50747
- writeStored(next);
50748
- }, [])];
51486
+ return usePersistedPageSize({
51487
+ storageKey: DEVICE_LIST_PAGE_SIZE_KEY,
51488
+ options: DEVICE_LIST_PAGE_SIZE_OPTIONS,
51489
+ defaultSize: DEVICE_LIST_PAGE_SIZE_OPTIONS.includes(defaultSize) ? defaultSize : 50
51490
+ });
50749
51491
  }
50750
51492
  //#endregion
50751
51493
  //#region src/composites/device-item/helpers.ts
@@ -53043,6 +53785,29 @@ function groupTopLevelAndAccessories(devices, options) {
53043
53785
  };
53044
53786
  }
53045
53787
  //#endregion
53788
+ //#region src/composites/device-list/page-size-select.tsx
53789
+ function pageSizeLabel(size) {
53790
+ return size === 0 ? "All" : String(size);
53791
+ }
53792
+ function PageSizeSelect({ value, options, onChange, ariaLabel }) {
53793
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
53794
+ className: "inline-flex items-center gap-1.5",
53795
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
53796
+ "aria-hidden": "true",
53797
+ children: "Per page"
53798
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
53799
+ "aria-label": ariaLabel,
53800
+ value,
53801
+ onChange: (e) => onChange(Number.parseInt(e.target.value, 10)),
53802
+ className: "h-7 px-1.5 rounded-md border border-border bg-surface text-[11px] text-foreground focus:outline-none focus:ring-1 focus:ring-primary/40",
53803
+ children: options.map((opt) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
53804
+ value: opt,
53805
+ children: pageSizeLabel(opt)
53806
+ }, opt))
53807
+ })]
53808
+ });
53809
+ }
53810
+ //#endregion
53046
53811
  //#region src/composites/device-list/multi-select.tsx
53047
53812
  /**
53048
53813
  * Compact multi-select dropdown — a single chip-sized trigger ("Label · N")
@@ -53957,23 +54722,11 @@ function DeviceModeList(props) {
53957
54722
  totalItems,
53958
54723
  " ",
53959
54724
  totalItems === 1 ? "device" : "devices"
53960
- ] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
53961
- className: "inline-flex items-center gap-1.5",
53962
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
53963
- "aria-hidden": "true",
53964
- children: "Per page"
53965
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
53966
- "aria-label": "Devices per page",
53967
- value: tablePageSize,
53968
- onChange: (e) => {
53969
- setTablePageSize(Number.parseInt(e.target.value, 10));
53970
- },
53971
- className: "h-7 px-1.5 rounded-md border border-border bg-surface text-[11px] text-foreground focus:outline-none focus:ring-1 focus:ring-primary/40",
53972
- children: DEVICE_LIST_PAGE_SIZE_OPTIONS.map((opt) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
53973
- value: opt,
53974
- children: opt
53975
- }, opt))
53976
- })]
54725
+ ] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PageSizeSelect, {
54726
+ ariaLabel: "Devices per page",
54727
+ value: tablePageSize,
54728
+ options: DEVICE_LIST_PAGE_SIZE_OPTIONS,
54729
+ onChange: setTablePageSize
53977
54730
  })]
53978
54731
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pagination, {
53979
54732
  currentPage: safePage,
@@ -54017,6 +54770,7 @@ function useDeviceListRealtimeSync() {
54017
54770
  }
54018
54771
  //#endregion
54019
54772
  //#region src/composites/device-list/generic-mode.tsx
54773
+ var FIXED_PAGE_SIZE = 100;
54020
54774
  /**
54021
54775
  * Generic render-prop list: search + pagination chrome shared with the device
54022
54776
  * list, over an arbitrary item type. Local (non-URL) state — intended for
@@ -54024,7 +54778,12 @@ function useDeviceListRealtimeSync() {
54024
54778
  * `renderItem`.
54025
54779
  */
54026
54780
  function GenericModeList(props) {
54027
- const { items, getKey, getSearchText, renderItem, getFilterType, mapFilterOption, filterOptions, filterLabel = "Type", initialTypeFilters = [], columns, renderExpanded, isExpanded, rowClassName, onRowClick, isLoading = false, searchPlaceholder = "Search", emptyLabel = "No items found.", pageSize = 100, toolbar } = props;
54781
+ const { items, getKey, getSearchText, renderItem, getFilterType, mapFilterOption, filterOptions, filterLabel = "Type", initialTypeFilters = [], columns, renderExpanded, isExpanded, rowClassName, onRowClick, isLoading = false, searchPlaceholder = "Search", emptyLabel = "No items found.", pageSize: fixedPageSize = FIXED_PAGE_SIZE, pageSizeSelector, toolbar } = props;
54782
+ const [chosenPageSize, setChosenPageSize] = usePersistedPageSize(pageSizeSelector ?? {
54783
+ storageKey: null,
54784
+ options: [fixedPageSize],
54785
+ defaultSize: fixedPageSize
54786
+ });
54028
54787
  const [searchInput, setSearchInput] = (0, react$1.useState)("");
54029
54788
  const debouncedSearch = useDebouncedString(searchInput, 300);
54030
54789
  const [typeFilters, setTypeFilters] = (0, react$1.useState)(() => [...initialTypeFilters]);
@@ -54073,6 +54832,7 @@ function GenericModeList(props) {
54073
54832
  getFilterType
54074
54833
  ]);
54075
54834
  const totalItems = filtered.length;
54835
+ const pageSize = pageSizeSelector ? effectivePageSize(chosenPageSize, totalItems) : fixedPageSize;
54076
54836
  const totalPages = Math.max(1, Math.ceil(totalItems / pageSize));
54077
54837
  const safePage = Math.min(page, totalPages);
54078
54838
  const startIdx = (safePage - 1) * pageSize;
@@ -54156,7 +54916,23 @@ function GenericModeList(props) {
54156
54916
  }),
54157
54917
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
54158
54918
  className: "shrink-0",
54159
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pagination, {
54919
+ children: pageSizeSelector && totalItems > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
54920
+ className: "flex items-center justify-between gap-4 flex-wrap pt-2 text-[11px] text-foreground-subtle",
54921
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
54922
+ className: "inline-flex items-center gap-2",
54923
+ children: [totalItems <= pageSize && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [totalItems, " total"] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PageSizeSelect, {
54924
+ ariaLabel: "Rows per page",
54925
+ value: chosenPageSize,
54926
+ options: pageSizeSelector.options,
54927
+ onChange: setChosenPageSize
54928
+ })]
54929
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pagination, {
54930
+ currentPage: safePage,
54931
+ totalItems,
54932
+ pageSize,
54933
+ onPageChange: setPage
54934
+ })]
54935
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pagination, {
54160
54936
  currentPage: safePage,
54161
54937
  totalItems,
54162
54938
  pageSize,
@@ -54285,6 +55061,24 @@ var DeviceItemRowSchema = zod.z.object({
54285
55061
  });
54286
55062
  var SupportedKindsSchema = zod.z.array(zod.z.string());
54287
55063
  var STATUS_POLL_INTERVAL_MS = 1e4;
55064
+ /** Where the viewer's "rows per page" choice for this table is remembered. */
55065
+ var DEVICE_EXPORT_PAGE_SIZE_KEY = "camstack.device-export.pageSize";
55066
+ /**
55067
+ * The exposed-devices table's page sizes. Its own key and options, not the
55068
+ * Devices page's (25/50/100/200, default 50): the operator asked for a short
55069
+ * default here (25) and an "All" for a quick scan of every exportable device.
55070
+ */
55071
+ var DEVICE_EXPORT_PAGE_SIZE = {
55072
+ options: [
55073
+ 10,
55074
+ 25,
55075
+ 50,
55076
+ 100,
55077
+ 0
55078
+ ],
55079
+ defaultSize: 25,
55080
+ storageKey: DEVICE_EXPORT_PAGE_SIZE_KEY
55081
+ };
54288
55082
  /**
54289
55083
  * The fleet-list poll. Deliberately the same 30 s as the window
54290
55084
  * `applyFleetQueryDefaults` registers for `deviceManager.listAll` (D274) —
@@ -54369,6 +55163,35 @@ function SetupSection({ setup }) {
54369
55163
  })]
54370
55164
  });
54371
55165
  }
55166
+ /** Parse `getStatus` against the canonical cap schema (loose wire output). */
55167
+ function parseStatus(data) {
55168
+ const parsed = _camstack_types.DeviceExportStatusSchema.safeParse(data);
55169
+ return parsed.success ? parsed.data : null;
55170
+ }
55171
+ /**
55172
+ * The exporter's error and Setup block on their own — for a page that shows
55173
+ * them above its settings while the devices table sits at the bottom. Renders
55174
+ * nothing when the exporter reports neither. Shares `getStatus`'s query key
55175
+ * with the panel, so mounting both costs one request.
55176
+ */
55177
+ function DeviceExportSetup({ addonId }) {
55178
+ const statusQuery = useDeviceExportGetStatus({
55179
+ nodeId: "hub",
55180
+ addonId
55181
+ }, {
55182
+ refetchInterval: STATUS_POLL_INTERVAL_MS,
55183
+ retry: false
55184
+ });
55185
+ const status = (0, react$1.useMemo)(() => parseStatus(statusQuery.data), [statusQuery.data]);
55186
+ if (!status?.error && !status?.setup) return null;
55187
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55188
+ className: "rounded-lg border border-border bg-surface overflow-hidden",
55189
+ children: [status.error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
55190
+ className: "px-2 sm:px-4 py-3 border-b border-border",
55191
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBox, { message: status.error })
55192
+ }), status.setup && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SetupSection, { setup: status.setup })]
55193
+ });
55194
+ }
54372
55195
  /**
54373
55196
  * Per-row Expose/Exposed toggle. Reflects whether the row's device is exposed
54374
55197
  * to THIS exporter and flips it through the SAME runner the bulk buttons use —
@@ -54447,7 +55270,7 @@ function BulkSummary({ outcome, onDismiss }) {
54447
55270
  * Generic device-export panel. Addon-agnostic — only the `device-export`
54448
55271
  * cap drives it.
54449
55272
  */
54450
- function DeviceExportPanel({ addonId, onOpenDevice }) {
55273
+ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
54451
55274
  const queryClient = (0, _tanstack_react_query.useQueryClient)();
54452
55275
  const statusQuery = useDeviceExportGetStatus({
54453
55276
  nodeId: "hub",
@@ -54473,10 +55296,7 @@ function DeviceExportPanel({ addonId, onOpenDevice }) {
54473
55296
  }, [queryClient]);
54474
55297
  const exposeMutation = useDeviceExportExposeDevice({ onSuccess: invalidate });
54475
55298
  const unexposeMutation = useDeviceExportUnexposeDevice({ onSuccess: invalidate });
54476
- const status = (0, react$1.useMemo)(() => {
54477
- const parsed = _camstack_types.DeviceExportStatusSchema.safeParse(statusQuery.data);
54478
- return parsed.success ? parsed.data : null;
54479
- }, [statusQuery.data]);
55299
+ const status = (0, react$1.useMemo)(() => parseStatus(statusQuery.data), [statusQuery.data]);
54480
55300
  const exposed = (0, react$1.useMemo)(() => {
54481
55301
  const parsed = ExposedDeviceArraySchema.safeParse(exposedQuery.data);
54482
55302
  return parsed.success ? parsed.data : [];
@@ -54699,11 +55519,11 @@ function DeviceExportPanel({ addonId, onOpenDevice }) {
54699
55519
  ]
54700
55520
  }), loading && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LoaderCircle, { className: "h-3.5 w-3.5 animate-spin text-foreground-subtle" })]
54701
55521
  }),
54702
- status?.error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
55522
+ showSetup && status?.error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
54703
55523
  className: "px-2 sm:px-4 py-3 border-b border-border",
54704
55524
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBox, { message: status.error })
54705
55525
  }),
54706
- status?.setup && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SetupSection, { setup: status.setup }),
55526
+ showSetup && status?.setup && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SetupSection, { setup: status.setup }),
54707
55527
  outcome !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BulkSummary, {
54708
55528
  outcome,
54709
55529
  onDismiss: dismissOutcome
@@ -54723,6 +55543,7 @@ function DeviceExportPanel({ addonId, onOpenDevice }) {
54723
55543
  columns,
54724
55544
  onRowClick: onOpenDevice ? onRowClick : void 0,
54725
55545
  toolbar: selectionToolbar,
55546
+ pageSizeSelector: DEVICE_EXPORT_PAGE_SIZE,
54726
55547
  searchPlaceholder: "Search by name or stableId",
54727
55548
  emptyLabel: "No device matches this search."
54728
55549
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmptyState, {
@@ -54748,145 +55569,6 @@ function DeviceGrid({ children, minCardWidth = 220, gap = 3, className }) {
54748
55569
  });
54749
55570
  }
54750
55571
  //#endregion
54751
- //#region src/composites/icon-picker.tsx
54752
- /**
54753
- * icon-picker — the ONE control for choosing an icon, wherever admin-ui asks.
54754
- *
54755
- * There were two: a device one over `DISPLAY_ICON_REGISTRY` and a room one
54756
- * over `LOCATION_ICON_IDS`, identical apart from the vocabulary they walked.
54757
- * That was tolerable while the device set was 56 glyphs in a flat grid. It is
54758
- * not at 157: a picker that only lets the operator HUNT is a picker they stop
54759
- * using, and the two copies were already drifting — only one of them had an
54760
- * i18n'd label.
54761
- *
54762
- * So the vocabulary, the grouping and the search all come from
54763
- * `@camstack/types`' `display-icon-vocabulary`, which is the same module the
54764
- * viewer's sheet searches. What is left here is the web half: a popover, a
54765
- * grid, and a text field.
54766
- *
54767
- * ## What it does NOT own
54768
- *
54769
- * - **The glyphs.** `resolveGlyph` is a prop, because this package's table is
54770
- * `lucide-react` and the viewer's is `lucide-react-native`. A key this build
54771
- * cannot draw is SKIPPED rather than rendered as a hole — an empty tile
54772
- * reads as a rendering bug, not as a missing choice.
54773
- * - **The words.** Every string is a prop. This package has no i18n (one file
54774
- * in it imports react-i18next, and that is the exception), and a control
54775
- * that reached for a translation would force it on every consumer.
54776
- * - **The write.** `onPick` is called and that is all; whether it is a form
54777
- * value or an immediate mutation is the caller's business, as it already was
54778
- * in both pickers this replaces.
54779
- *
54780
- * ## The clear tile is never filtered away
54781
- *
54782
- * The "inherit" / "no icon" choice sits above the sections and ignores the
54783
- * search. A query that hid it would strand the operator on a glyph they picked
54784
- * by accident with no way back to the default — the one action a picker must
54785
- * always offer.
54786
- */
54787
- function IconPicker({ value, onPick, resolveGlyph, fallbackGlyph, labels, only, extraWords, clearGlyph, labelFor, disabled = false }) {
54788
- const [open, setOpen] = (0, react$1.useState)(false);
54789
- const [query, setQuery] = (0, react$1.useState)("");
54790
- const allowed = (0, react$1.useMemo)(() => only === void 0 ? null : new Set(only), [only]);
54791
- const sections = (0, react$1.useMemo)(() => {
54792
- const hits = (0, _camstack_types.searchDisplayIcons)(query, extraWords);
54793
- const byGroup = /* @__PURE__ */ new Map();
54794
- for (const hit of hits) {
54795
- const { key, group } = hit.entry;
54796
- if (allowed !== null && !allowed.has(key)) continue;
54797
- if (resolveGlyph(key) === void 0) continue;
54798
- const bucket = byGroup.get(group);
54799
- if (bucket === void 0) byGroup.set(group, [key]);
54800
- else bucket.push(key);
54801
- }
54802
- return _camstack_types.DISPLAY_ICON_GROUPS.flatMap((group) => {
54803
- const keys = byGroup.get(group);
54804
- return keys === void 0 || keys.length === 0 ? [] : [{
54805
- group,
54806
- keys
54807
- }];
54808
- });
54809
- }, [
54810
- query,
54811
- extraWords,
54812
- allowed,
54813
- resolveGlyph
54814
- ]);
54815
- const Current = (value === null ? void 0 : resolveGlyph(value)) ?? fallbackGlyph;
54816
- const choose = async (next) => {
54817
- setOpen(false);
54818
- setQuery("");
54819
- await onPick(next);
54820
- };
54821
- const Clear = clearGlyph ?? fallbackGlyph;
54822
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
54823
- className: "relative flex shrink-0 items-center gap-2",
54824
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
54825
- type: "button",
54826
- disabled,
54827
- onClick: () => setOpen((o) => !o),
54828
- "aria-expanded": open,
54829
- "aria-label": labels.trigger,
54830
- title: labels.trigger,
54831
- className: "rounded border border-border p-1 text-foreground-muted hover:bg-surface-hover disabled:opacity-50",
54832
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Current, { className: "h-4 w-4" })
54833
- }), open && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
54834
- type: "button",
54835
- "aria-label": labels.close,
54836
- onClick: () => setOpen(false),
54837
- className: "fixed inset-0 z-10 cursor-default"
54838
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
54839
- role: "group",
54840
- "aria-label": labels.panel,
54841
- className: "absolute left-0 top-full z-20 mt-1 max-h-80 w-72 overflow-y-auto rounded-md border border-border bg-surface p-2 shadow-lg",
54842
- children: [
54843
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
54844
- type: "search",
54845
- value: query,
54846
- autoFocus: true,
54847
- onChange: (e) => setQuery(e.target.value),
54848
- placeholder: labels.searchPlaceholder,
54849
- "aria-label": labels.searchPlaceholder,
54850
- className: "mb-2 w-full rounded border border-border bg-surface-raised px-2 py-1 text-xs text-foreground placeholder:text-foreground-subtle"
54851
- }),
54852
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
54853
- type: "button",
54854
- onClick: () => void choose(null),
54855
- "aria-pressed": value === null,
54856
- className: "mb-2 flex w-full items-center gap-1.5 rounded px-1.5 py-1 text-[11px] text-foreground-subtle hover:bg-surface-hover",
54857
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Clear, { className: "h-3.5 w-3.5" }), labels.clear]
54858
- }),
54859
- sections.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
54860
- className: "px-1.5 py-2 text-[11px] text-foreground-subtle",
54861
- children: labels.noMatches
54862
- }) : sections.map(({ group, keys }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
54863
- className: "mb-2 last:mb-0",
54864
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
54865
- className: "px-1.5 pb-1 text-[10px] uppercase tracking-wide text-foreground-subtle",
54866
- children: labels.groups[group]
54867
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
54868
- className: "grid grid-cols-7 gap-1",
54869
- children: keys.map((key) => {
54870
- const Glyph = resolveGlyph(key);
54871
- if (Glyph === void 0) return null;
54872
- const selected = value === key;
54873
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
54874
- type: "button",
54875
- onClick: () => void choose(key),
54876
- "aria-pressed": selected,
54877
- "aria-label": labelFor?.(key) ?? key,
54878
- title: labelFor?.(key) ?? key,
54879
- className: selected ? "flex items-center justify-center rounded border border-primary bg-primary/10 p-1.5 text-foreground" : "flex items-center justify-center rounded border border-transparent p-1.5 text-foreground-muted hover:bg-surface-hover",
54880
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Glyph, { className: "h-4 w-4" })
54881
- }, key);
54882
- })
54883
- })]
54884
- }, group))
54885
- ]
54886
- })] })]
54887
- });
54888
- }
54889
- //#endregion
54890
55572
  //#region src/composites/device-selector-picker.tsx
54891
55573
  /**
54892
55574
  * <DeviceSelectorPicker> — the operator-facing editor for a v3
@@ -61087,6 +61769,7 @@ exports.CoverPanel = CoverPanel;
61087
61769
  exports.CustomFieldRenderersProvider = CustomFieldRenderersProvider;
61088
61770
  exports.DEFAULT_COLOR = DEFAULT_COLOR;
61089
61771
  exports.DEVICE_COLUMNS = DEVICE_COLUMNS;
61772
+ exports.DEVICE_EXPORT_PAGE_SIZE_KEY = DEVICE_EXPORT_PAGE_SIZE_KEY;
61090
61773
  exports.DEVICE_LIST_PAGE_SIZE_KEY = DEVICE_LIST_PAGE_SIZE_KEY;
61091
61774
  exports.DEVICE_LIST_PAGE_SIZE_OPTIONS = DEVICE_LIST_PAGE_SIZE_OPTIONS;
61092
61775
  exports.DEVICE_PICKER_GROUP_STORAGE_KEY = DEVICE_PICKER_GROUP_STORAGE_KEY;
@@ -61105,6 +61788,7 @@ exports.DeviceBatchToolbar = DeviceBatchToolbar;
61105
61788
  exports.DeviceCard = DeviceCard;
61106
61789
  exports.DeviceContextProvider = DeviceContextProvider;
61107
61790
  exports.DeviceExportPanel = DeviceExportPanel;
61791
+ exports.DeviceExportSetup = DeviceExportSetup;
61108
61792
  exports.DeviceFilterBar = DeviceFilterBar;
61109
61793
  exports.DeviceGrid = DeviceGrid;
61110
61794
  exports.DeviceItem = DeviceItem;
@@ -61218,6 +61902,7 @@ exports.NotificationTargetsField = NotificationTargetsField;
61218
61902
  exports.OPTION_SELECT_SEARCH_MIN_OPTIONS = OPTION_SELECT_SEARCH_MIN_OPTIONS;
61219
61903
  exports.OfflineBadge = OfflineBadge;
61220
61904
  exports.OptionSelect = OptionSelect;
61905
+ exports.PAGE_SIZE_ALL = PAGE_SIZE_ALL;
61221
61906
  exports.PHASE_CONFIG = PHASE_CONFIG;
61222
61907
  exports.PICKER_CHIP_LIMIT = PICKER_CHIP_LIMIT;
61223
61908
  exports.PICKER_DEFAULT_GROUP_BY = PICKER_DEFAULT_GROUP_BY;
@@ -61232,6 +61917,7 @@ exports.PICKER_WINDOW = PICKER_WINDOW;
61232
61917
  exports.PRIORITY = PRIORITY;
61233
61918
  exports.PTZOverlay = PTZOverlay;
61234
61919
  exports.PageHeader = PageHeader;
61920
+ exports.PageSizeSelect = PageSizeSelect;
61235
61921
  exports.PhaseIcon = PhaseIcon;
61236
61922
  exports.PipelineBuilder = PipelineBuilder;
61237
61923
  exports.PipelineRuntimeSelector = PipelineRuntimeSelector;
@@ -61264,6 +61950,7 @@ exports.RotateCcw = RotateCcw;
61264
61950
  exports.RunReport = RunReport;
61265
61951
  exports.SCENE_CAPTURE_VARIANTS = SCENE_CAPTURE_VARIANTS;
61266
61952
  exports.SCENE_CAPTURE_VARIANTS$1 = SCENE_CAPTURE_VARIANTS;
61953
+ exports.SECRET_REVEAL_MS = SECRET_REVEAL_MS;
61267
61954
  exports.SECTION_BODY = SECTION_BODY;
61268
61955
  exports.SECTION_CARD = SECTION_CARD;
61269
61956
  exports.SECTION_HEADER = SECTION_HEADER;
@@ -61283,11 +61970,14 @@ exports.ScopePicker = ScopePicker;
61283
61970
  exports.ScriptHeroCard = ScriptHeroCard;
61284
61971
  exports.ScriptInlineControl = ScriptInlineControl;
61285
61972
  exports.ScrollArea = ScrollArea;
61973
+ exports.SecretFieldControl = SecretFieldControl;
61974
+ exports.SecretRevealProvider = SecretRevealProvider;
61286
61975
  exports.SectionCard = SectionCard;
61287
61976
  exports.SegmentedChoice = SegmentedChoice;
61288
61977
  exports.Select = Select;
61289
61978
  exports.SelectedChip = SelectedChip;
61290
61979
  exports.SemanticBadge = SemanticBadge;
61980
+ exports.SendFrameToTrainingButton = SendFrameToTrainingButton;
61291
61981
  exports.SensorHeroCard = SensorHeroCard;
61292
61982
  exports.SensorInlineControl = SensorInlineControl;
61293
61983
  exports.SensorValueAtom = SensorValueAtom;
@@ -61335,6 +62025,7 @@ exports.TimezoneSelector = TimezoneSelector;
61335
62025
  exports.Tooltip = Tooltip;
61336
62026
  exports.TooltipContent = TooltipContent;
61337
62027
  exports.TooltipTrigger = TooltipTrigger;
62028
+ exports.TrainingCollectionPicker = TrainingCollectionPicker;
61338
62029
  exports.Trash2 = Trash2;
61339
62030
  exports.UnitPicker = UnitPicker;
61340
62031
  exports.VacuumHeroCard = VacuumHeroCard;
@@ -61404,6 +62095,7 @@ exports.deviceTypeMetaOf = deviceTypeMetaOf;
61404
62095
  exports.devicesToOptions = devicesToOptions;
61405
62096
  exports.dockBox = dockBox;
61406
62097
  exports.draftFromQuery = draftFromQuery;
62098
+ exports.effectivePageSize = effectivePageSize;
61407
62099
  exports.emptyDevicesWidens = emptyDevicesWidens;
61408
62100
  exports.emptySessionRun = emptySessionRun;
61409
62101
  exports.ensureMfHostInit = ensureMfHostInit;
@@ -61444,6 +62136,7 @@ exports.isAbsentProvider$1 = isAbsentProvider;
61444
62136
  exports.isDataTableFilterActive = isDataTableFilterActive;
61445
62137
  exports.isFieldVisible = isFieldVisible;
61446
62138
  exports.isGroupExpanded = isGroupExpanded;
62139
+ exports.isKeychainFieldValue = isKeychainFieldValue;
61447
62140
  exports.isValueField = isValueField;
61448
62141
  exports.lawnMowerActivityMeta = lawnMowerActivityMeta;
61449
62142
  exports.lightColors = require_theme_index.lightColors$1;
@@ -61466,6 +62159,7 @@ exports.optionSelectSearchText = optionSearchText;
61466
62159
  exports.orphanDeviceLabel = orphanDeviceLabel;
61467
62160
  exports.overrideEntityIdFromLink = overrideEntityIdFromLink;
61468
62161
  exports.pageRows = pageRows;
62162
+ exports.pageSizeLabel = pageSizeLabel;
61469
62163
  exports.parentDetailOf = parentDetailOf;
61470
62164
  exports.parseIceCandidate = parseIceCandidate;
61471
62165
  exports.parsePickerGroupBy = parsePickerGroupBy;
@@ -61539,6 +62233,7 @@ exports.toStationaryOverlayBox = toStationaryOverlayBox;
61539
62233
  exports.toggleDataTableFacet = toggleDataTableFacet;
61540
62234
  exports.togglePickerDevice = togglePickerDevice;
61541
62235
  exports.togglePickerKey = togglePickerKey;
62236
+ exports.trainingRefusalText = trainingRefusalText;
61542
62237
  exports.trpc = trpc;
61543
62238
  exports.typeLabel = typeLabel;
61544
62239
  exports.useAccessoriesGetStatus = useAccessoriesGetStatus;
@@ -61971,6 +62666,15 @@ exports.useIntercomStartTalkSession = useIntercomStartTalkSession;
61971
62666
  exports.useIntercomStopSession = useIntercomStopSession;
61972
62667
  exports.useIsMidWidth = useIsMidWidth;
61973
62668
  exports.useIsMobile = useIsMobile;
62669
+ exports.useKeychainAdminDeleteOrphans = useKeychainAdminDeleteOrphans;
62670
+ exports.useKeychainAdminDownloadRecoveryKey = useKeychainAdminDownloadRecoveryKey;
62671
+ exports.useKeychainAdminListAudit = useKeychainAdminListAudit;
62672
+ exports.useKeychainAdminListSecrets = useKeychainAdminListSecrets;
62673
+ exports.useKeychainAdminPurgeRetiredKeys = useKeychainAdminPurgeRetiredKeys;
62674
+ exports.useKeychainAdminResetKeychain = useKeychainAdminResetKeychain;
62675
+ exports.useKeychainAdminReveal = useKeychainAdminReveal;
62676
+ exports.useKeychainAdminRotateMasterKey = useKeychainAdminRotateMasterKey;
62677
+ exports.useKeychainAdminStatus = useKeychainAdminStatus;
61974
62678
  exports.useLawnMowerControlDock = useLawnMowerControlDock;
61975
62679
  exports.useLawnMowerControlGetStatus = useLawnMowerControlGetStatus;
61976
62680
  exports.useLawnMowerControlPause = useLawnMowerControlPause;
@@ -62176,6 +62880,7 @@ exports.useOsdManagerSetSlotBinding = useOsdManagerSetSlotBinding;
62176
62880
  exports.useOsdManagerUpsertTemplate = useOsdManagerUpsertTemplate;
62177
62881
  exports.useOsdSetOverlay = useOsdSetOverlay;
62178
62882
  exports.usePTZ = usePTZ;
62883
+ exports.usePersistedPageSize = usePersistedPageSize;
62179
62884
  exports.usePetFeederCallPet = usePetFeederCallPet;
62180
62885
  exports.usePetFeederCancelFeed = usePetFeederCancelFeed;
62181
62886
  exports.usePetFeederFeed = usePetFeederFeed;
@@ -62188,6 +62893,8 @@ exports.usePetFeederSetFeedSound = usePetFeederSetFeedSound;
62188
62893
  exports.usePetFeederSetIndicatorLight = usePetFeederSetIndicatorLight;
62189
62894
  exports.usePetFeederSetVolume = usePetFeederSetVolume;
62190
62895
  exports.usePickerSiteZones = usePickerSiteZones;
62896
+ exports.usePipelineAnalyticsAddRetrainFrameFromRecording = usePipelineAnalyticsAddRetrainFrameFromRecording;
62897
+ exports.usePipelineAnalyticsAddRetrainFramesToCollection = usePipelineAnalyticsAddRetrainFramesToCollection;
62191
62898
  exports.usePipelineAnalyticsApplyDeviceSettingsPatch = usePipelineAnalyticsApplyDeviceSettingsPatch;
62192
62899
  exports.usePipelineAnalyticsCancelRelocateMedia = usePipelineAnalyticsCancelRelocateMedia;
62193
62900
  exports.usePipelineAnalyticsCancelStorageMigrationMove = usePipelineAnalyticsCancelStorageMigrationMove;
@@ -62196,7 +62903,9 @@ exports.usePipelineAnalyticsClearTracks = usePipelineAnalyticsClearTracks;
62196
62903
  exports.usePipelineAnalyticsCompleteRetrainTrack = usePipelineAnalyticsCompleteRetrainTrack;
62197
62904
  exports.usePipelineAnalyticsCountRelocatableMedia = usePipelineAnalyticsCountRelocatableMedia;
62198
62905
  exports.usePipelineAnalyticsCountUnstampedEventMedia = usePipelineAnalyticsCountUnstampedEventMedia;
62906
+ exports.usePipelineAnalyticsCreateRetrainCollection = usePipelineAnalyticsCreateRetrainCollection;
62199
62907
  exports.usePipelineAnalyticsDeleteDeviceEvents = usePipelineAnalyticsDeleteDeviceEvents;
62908
+ exports.usePipelineAnalyticsDeleteRetrainCollection = usePipelineAnalyticsDeleteRetrainCollection;
62200
62909
  exports.usePipelineAnalyticsDeleteTracks = usePipelineAnalyticsDeleteTracks;
62201
62910
  exports.usePipelineAnalyticsDeselectRetrainFrame = usePipelineAnalyticsDeselectRetrainFrame;
62202
62911
  exports.usePipelineAnalyticsGetActiveTracks = usePipelineAnalyticsGetActiveTracks;
@@ -62231,6 +62940,9 @@ exports.usePipelineAnalyticsListOpsLog = usePipelineAnalyticsListOpsLog;
62231
62940
  exports.usePipelineAnalyticsListRecentTracks = usePipelineAnalyticsListRecentTracks;
62232
62941
  exports.usePipelineAnalyticsListRelocateMediaJobs = usePipelineAnalyticsListRelocateMediaJobs;
62233
62942
  exports.usePipelineAnalyticsListRetrainAnnotations = usePipelineAnalyticsListRetrainAnnotations;
62943
+ exports.usePipelineAnalyticsListRetrainAssistProfiles = usePipelineAnalyticsListRetrainAssistProfiles;
62944
+ exports.usePipelineAnalyticsListRetrainCollectionFrames = usePipelineAnalyticsListRetrainCollectionFrames;
62945
+ exports.usePipelineAnalyticsListRetrainCollections = usePipelineAnalyticsListRetrainCollections;
62234
62946
  exports.usePipelineAnalyticsListRetrainFrames = usePipelineAnalyticsListRetrainFrames;
62235
62947
  exports.usePipelineAnalyticsListRetrainStaging = usePipelineAnalyticsListRetrainStaging;
62236
62948
  exports.usePipelineAnalyticsListTrackMedia = usePipelineAnalyticsListTrackMedia;
@@ -62239,6 +62951,7 @@ exports.usePipelineAnalyticsListTracks = usePipelineAnalyticsListTracks;
62239
62951
  exports.usePipelineAnalyticsOwnersWithMedia = usePipelineAnalyticsOwnersWithMedia;
62240
62952
  exports.usePipelineAnalyticsPauseForStorageMigration = usePipelineAnalyticsPauseForStorageMigration;
62241
62953
  exports.usePipelineAnalyticsProposeRetrainAnnotations = usePipelineAnalyticsProposeRetrainAnnotations;
62954
+ exports.usePipelineAnalyticsProposeRetrainAnnotationsWithLlm = usePipelineAnalyticsProposeRetrainAnnotationsWithLlm;
62242
62955
  exports.usePipelineAnalyticsPruneEvents = usePipelineAnalyticsPruneEvents;
62243
62956
  exports.usePipelineAnalyticsPruneEventsBefore = usePipelineAnalyticsPruneEventsBefore;
62244
62957
  exports.usePipelineAnalyticsPruneTracksBefore = usePipelineAnalyticsPruneTracksBefore;
@@ -62248,17 +62961,21 @@ exports.usePipelineAnalyticsReclaimDebugMedia = usePipelineAnalyticsReclaimDebug
62248
62961
  exports.usePipelineAnalyticsReconcileFromDisk = usePipelineAnalyticsReconcileFromDisk;
62249
62962
  exports.usePipelineAnalyticsRefreshStorageLocationsForMigration = usePipelineAnalyticsRefreshStorageLocationsForMigration;
62250
62963
  exports.usePipelineAnalyticsRelocateMedia = usePipelineAnalyticsRelocateMedia;
62964
+ exports.usePipelineAnalyticsRemoveRetrainFrameFromCollection = usePipelineAnalyticsRemoveRetrainFrameFromCollection;
62965
+ exports.usePipelineAnalyticsRenameRetrainCollection = usePipelineAnalyticsRenameRetrainCollection;
62251
62966
  exports.usePipelineAnalyticsRestageRetrainTrack = usePipelineAnalyticsRestageRetrainTrack;
62252
62967
  exports.usePipelineAnalyticsResumeForStorageMigration = usePipelineAnalyticsResumeForStorageMigration;
62253
62968
  exports.usePipelineAnalyticsSaveRetrainAnnotations = usePipelineAnalyticsSaveRetrainAnnotations;
62254
62969
  exports.usePipelineAnalyticsSearchObjectEvents = usePipelineAnalyticsSearchObjectEvents;
62255
62970
  exports.usePipelineAnalyticsSelectRetrainFrames = usePipelineAnalyticsSelectRetrainFrames;
62971
+ exports.usePipelineAnalyticsSetRetrainFrameComplete = usePipelineAnalyticsSetRetrainFrameComplete;
62256
62972
  exports.usePipelineAnalyticsSetTrackFlags = usePipelineAnalyticsSetTrackFlags;
62257
62973
  exports.usePipelineAnalyticsSetTrackPurity = usePipelineAnalyticsSetTrackPurity;
62258
62974
  exports.usePipelineAnalyticsSetTrackRelation = usePipelineAnalyticsSetTrackRelation;
62259
62975
  exports.usePipelineAnalyticsStartStorageMigrationMove = usePipelineAnalyticsStartStorageMigrationMove;
62260
62976
  exports.usePipelineAnalyticsWipeAllAnalytics = usePipelineAnalyticsWipeAllAnalytics;
62261
62977
  exports.usePipelineAnalyticsWipeObjectEmbeddings = usePipelineAnalyticsWipeObjectEmbeddings;
62978
+ exports.usePipelineAnalyticsWriteRetrainArchive = usePipelineAnalyticsWriteRetrainArchive;
62262
62979
  exports.usePipelineExecutorCacheFrameInPool = usePipelineExecutorCacheFrameInPool;
62263
62980
  exports.usePipelineExecutorClearDeviceOverrides = usePipelineExecutorClearDeviceOverrides;
62264
62981
  exports.usePipelineExecutorClearModelBoundDeviceOverrides = usePipelineExecutorClearModelBoundDeviceOverrides;
@@ -62294,10 +63011,12 @@ exports.usePipelineExecutorSetVideoPipelineSteps = usePipelineExecutorSetVideoPi
62294
63011
  exports.usePipelineExecutorSpinEngine = usePipelineExecutorSpinEngine;
62295
63012
  exports.usePipelineExecutorUncacheFrame = usePipelineExecutorUncacheFrame;
62296
63013
  exports.usePipelineExecutorValidatePipeline = usePipelineExecutorValidatePipeline;
63014
+ exports.usePipelineOrchestratorAbortNodeBenchmark = usePipelineOrchestratorAbortNodeBenchmark;
62297
63015
  exports.usePipelineOrchestratorApplyAutoTune = usePipelineOrchestratorApplyAutoTune;
62298
63016
  exports.usePipelineOrchestratorApplyDeviceSettingsPatch = usePipelineOrchestratorApplyDeviceSettingsPatch;
62299
63017
  exports.usePipelineOrchestratorAssignAudio = usePipelineOrchestratorAssignAudio;
62300
63018
  exports.usePipelineOrchestratorAssignPipeline = usePipelineOrchestratorAssignPipeline;
63019
+ exports.usePipelineOrchestratorDeleteNodeBenchmarkResult = usePipelineOrchestratorDeleteNodeBenchmarkResult;
62301
63020
  exports.usePipelineOrchestratorGetAgentLoad = usePipelineOrchestratorGetAgentLoad;
62302
63021
  exports.usePipelineOrchestratorGetAgentSettings = usePipelineOrchestratorGetAgentSettings;
62303
63022
  exports.usePipelineOrchestratorGetAudioAssignment = usePipelineOrchestratorGetAudioAssignment;
@@ -62316,6 +63035,9 @@ exports.usePipelineOrchestratorGetDeviceSettingsContribution = usePipelineOrches
62316
63035
  exports.usePipelineOrchestratorGetEffectiveShares = usePipelineOrchestratorGetEffectiveShares;
62317
63036
  exports.usePipelineOrchestratorGetGlobalMetrics = usePipelineOrchestratorGetGlobalMetrics;
62318
63037
  exports.usePipelineOrchestratorGetIngestOwner = usePipelineOrchestratorGetIngestOwner;
63038
+ exports.usePipelineOrchestratorGetNodeBenchmarkCandidates = usePipelineOrchestratorGetNodeBenchmarkCandidates;
63039
+ exports.usePipelineOrchestratorGetNodeBenchmarkPreflight = usePipelineOrchestratorGetNodeBenchmarkPreflight;
63040
+ exports.usePipelineOrchestratorGetNodeBenchmarkStatus = usePipelineOrchestratorGetNodeBenchmarkStatus;
62319
63041
  exports.usePipelineOrchestratorGetNodeInferenceDevices = usePipelineOrchestratorGetNodeInferenceDevices;
62320
63042
  exports.usePipelineOrchestratorGetPipelineAssignment = usePipelineOrchestratorGetPipelineAssignment;
62321
63043
  exports.usePipelineOrchestratorGetPipelineAssignments = usePipelineOrchestratorGetPipelineAssignments;
@@ -62323,6 +63045,7 @@ exports.usePipelineOrchestratorGetPipelineDevicePin = usePipelineOrchestratorGet
62323
63045
  exports.usePipelineOrchestratorGetPlacementStats = usePipelineOrchestratorGetPlacementStats;
62324
63046
  exports.usePipelineOrchestratorGetReconcileFromDiskStatus = usePipelineOrchestratorGetReconcileFromDiskStatus;
62325
63047
  exports.usePipelineOrchestratorListAgentSettings = usePipelineOrchestratorListAgentSettings;
63048
+ exports.usePipelineOrchestratorListNodeBenchmarkResults = usePipelineOrchestratorListNodeBenchmarkResults;
62326
63049
  exports.usePipelineOrchestratorPauseForStorageMigration = usePipelineOrchestratorPauseForStorageMigration;
62327
63050
  exports.usePipelineOrchestratorPlanAutoTune = usePipelineOrchestratorPlanAutoTune;
62328
63051
  exports.usePipelineOrchestratorRebalance = usePipelineOrchestratorRebalance;
@@ -62347,9 +63070,11 @@ exports.usePipelineOrchestratorSetCapabilityBinding = usePipelineOrchestratorSet
62347
63070
  exports.usePipelineOrchestratorSetLoadBalancingShares = usePipelineOrchestratorSetLoadBalancingShares;
62348
63071
  exports.usePipelineOrchestratorSetPipelineDevicePin = usePipelineOrchestratorSetPipelineDevicePin;
62349
63072
  exports.usePipelineOrchestratorSetShareSource = usePipelineOrchestratorSetShareSource;
63073
+ exports.usePipelineOrchestratorStartNodeBenchmark = usePipelineOrchestratorStartNodeBenchmark;
62350
63074
  exports.usePipelineOrchestratorSuggestShares = usePipelineOrchestratorSuggestShares;
62351
63075
  exports.usePipelineOrchestratorUnassignAudio = usePipelineOrchestratorUnassignAudio;
62352
63076
  exports.usePipelineOrchestratorUnassignPipeline = usePipelineOrchestratorUnassignPipeline;
63077
+ exports.usePipelineRunnerAbortNodeBench = usePipelineRunnerAbortNodeBench;
62353
63078
  exports.usePipelineRunnerAttachCamera = usePipelineRunnerAttachCamera;
62354
63079
  exports.usePipelineRunnerDetachCamera = usePipelineRunnerDetachCamera;
62355
63080
  exports.usePipelineRunnerGetAllCameraMetrics = usePipelineRunnerGetAllCameraMetrics;
@@ -62358,6 +63083,7 @@ exports.usePipelineRunnerGetLocalCameras = usePipelineRunnerGetLocalCameras;
62358
63083
  exports.usePipelineRunnerGetLocalLoad = usePipelineRunnerGetLocalLoad;
62359
63084
  exports.usePipelineRunnerGetLocalMetrics = usePipelineRunnerGetLocalMetrics;
62360
63085
  exports.usePipelineRunnerGetNativeCrop = usePipelineRunnerGetNativeCrop;
63086
+ exports.usePipelineRunnerGetNodeBenchStatus = usePipelineRunnerGetNodeBenchStatus;
62361
63087
  exports.usePipelineRunnerGetParkedTrackFrame = usePipelineRunnerGetParkedTrackFrame;
62362
63088
  exports.usePipelineRunnerParkTrackFrame = usePipelineRunnerParkTrackFrame;
62363
63089
  exports.usePipelineRunnerReleaseParkedTrackFrames = usePipelineRunnerReleaseParkedTrackFrames;
@@ -62365,6 +63091,7 @@ exports.usePipelineRunnerReportMotion = usePipelineRunnerReportMotion;
62365
63091
  exports.usePipelineRunnerRequestOccupancyBurst = usePipelineRunnerRequestOccupancyBurst;
62366
63092
  exports.usePipelineRunnerRunDetailSubtree = usePipelineRunnerRunDetailSubtree;
62367
63093
  exports.usePipelineRunnerRunStatelessStep = usePipelineRunnerRunStatelessStep;
63094
+ exports.usePipelineRunnerStartNodeBench = usePipelineRunnerStartNodeBench;
62368
63095
  exports.usePlateGalleryAssignPlate = usePlateGalleryAssignPlate;
62369
63096
  exports.usePlateGalleryAssignPlates = usePlateGalleryAssignPlates;
62370
63097
  exports.usePlateGalleryCorrectPlateText = usePlateGalleryCorrectPlateText;
@@ -62640,8 +63367,11 @@ exports.useUserManagementCreateScopedToken = useUserManagementCreateScopedToken;
62640
63367
  exports.useUserManagementCreateUser = useUserManagementCreateUser;
62641
63368
  exports.useUserManagementDeleteUser = useUserManagementDeleteUser;
62642
63369
  exports.useUserManagementDisableTotp = useUserManagementDisableTotp;
63370
+ exports.useUserManagementFindUserByExternalIdentity = useUserManagementFindUserByExternalIdentity;
62643
63371
  exports.useUserManagementGetTotpStatus = useUserManagementGetTotpStatus;
63372
+ exports.useUserManagementLinkExternalIdentity = useUserManagementLinkExternalIdentity;
62644
63373
  exports.useUserManagementListApiKeys = useUserManagementListApiKeys;
63374
+ exports.useUserManagementListExternalIdentities = useUserManagementListExternalIdentities;
62645
63375
  exports.useUserManagementListOauthSessions = useUserManagementListOauthSessions;
62646
63376
  exports.useUserManagementListScopedTokens = useUserManagementListScopedTokens;
62647
63377
  exports.useUserManagementListUsers = useUserManagementListUsers;
@@ -62655,6 +63385,7 @@ exports.useUserManagementRevokeOauthSession = useUserManagementRevokeOauthSessio
62655
63385
  exports.useUserManagementRevokeScopedToken = useUserManagementRevokeScopedToken;
62656
63386
  exports.useUserManagementSetUserScopes = useUserManagementSetUserScopes;
62657
63387
  exports.useUserManagementSetupTotp = useUserManagementSetupTotp;
63388
+ exports.useUserManagementUnlinkExternalIdentity = useUserManagementUnlinkExternalIdentity;
62658
63389
  exports.useUserManagementUpdateUser = useUserManagementUpdateUser;
62659
63390
  exports.useUserManagementValidateApiKey = useUserManagementValidateApiKey;
62660
63391
  exports.useUserManagementValidateCredentials = useUserManagementValidateCredentials;