@camstack/ui-library 1.1.6 → 1.1.8

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
@@ -10784,6 +10784,26 @@ var Smartphone = createLucideIcon("smartphone", [["rect", {
10784
10784
  * This source code is licensed under the ISC license.
10785
10785
  * See the LICENSE file in the root directory of this source tree.
10786
10786
  */
10787
+ var Sprout = createLucideIcon("sprout", [
10788
+ ["path", {
10789
+ d: "M14 9.536V7a4 4 0 0 1 4-4h1.5a.5.5 0 0 1 .5.5V5a4 4 0 0 1-4 4 4 4 0 0 0-4 4c0 2 1 3 1 5a5 5 0 0 1-1 3",
10790
+ key: "139s4v"
10791
+ }],
10792
+ ["path", {
10793
+ d: "M4 9a5 5 0 0 1 8 4 5 5 0 0 1-8-4",
10794
+ key: "1dlkgp"
10795
+ }],
10796
+ ["path", {
10797
+ d: "M5 21h14",
10798
+ key: "11awu3"
10799
+ }]
10800
+ ]);
10801
+ /**
10802
+ * @license lucide-react v0.576.0 - ISC
10803
+ *
10804
+ * This source code is licensed under the ISC license.
10805
+ * See the LICENSE file in the root directory of this source tree.
10806
+ */
10787
10807
  var Square = createLucideIcon("square", [["rect", {
10788
10808
  width: "18",
10789
10809
  height: "18",
@@ -10808,6 +10828,52 @@ var Star = createLucideIcon("star", [["path", {
10808
10828
  * This source code is licensed under the ISC license.
10809
10829
  * See the LICENSE file in the root directory of this source tree.
10810
10830
  */
10831
+ var SunMedium = createLucideIcon("sun-medium", [
10832
+ ["circle", {
10833
+ cx: "12",
10834
+ cy: "12",
10835
+ r: "4",
10836
+ key: "4exip2"
10837
+ }],
10838
+ ["path", {
10839
+ d: "M12 3v1",
10840
+ key: "1asbbs"
10841
+ }],
10842
+ ["path", {
10843
+ d: "M12 20v1",
10844
+ key: "1wcdkc"
10845
+ }],
10846
+ ["path", {
10847
+ d: "M3 12h1",
10848
+ key: "lp3yf2"
10849
+ }],
10850
+ ["path", {
10851
+ d: "M20 12h1",
10852
+ key: "1vloll"
10853
+ }],
10854
+ ["path", {
10855
+ d: "m18.364 5.636-.707.707",
10856
+ key: "1hakh0"
10857
+ }],
10858
+ ["path", {
10859
+ d: "m6.343 17.657-.707.707",
10860
+ key: "18m9nf"
10861
+ }],
10862
+ ["path", {
10863
+ d: "m5.636 5.636.707.707",
10864
+ key: "1xv1c5"
10865
+ }],
10866
+ ["path", {
10867
+ d: "m17.657 17.657.707.707",
10868
+ key: "vl76zb"
10869
+ }]
10870
+ ]);
10871
+ /**
10872
+ * @license lucide-react v0.576.0 - ISC
10873
+ *
10874
+ * This source code is licensed under the ISC license.
10875
+ * See the LICENSE file in the root directory of this source tree.
10876
+ */
10811
10877
  var Sun = createLucideIcon("sun", [
10812
10878
  ["circle", {
10813
10879
  cx: "12",
@@ -10854,6 +10920,46 @@ var Sun = createLucideIcon("sun", [
10854
10920
  * This source code is licensed under the ISC license.
10855
10921
  * See the LICENSE file in the root directory of this source tree.
10856
10922
  */
10923
+ var Sunrise = createLucideIcon("sunrise", [
10924
+ ["path", {
10925
+ d: "M12 2v8",
10926
+ key: "1q4o3n"
10927
+ }],
10928
+ ["path", {
10929
+ d: "m4.93 10.93 1.41 1.41",
10930
+ key: "2a7f42"
10931
+ }],
10932
+ ["path", {
10933
+ d: "M2 18h2",
10934
+ key: "j10viu"
10935
+ }],
10936
+ ["path", {
10937
+ d: "M20 18h2",
10938
+ key: "wocana"
10939
+ }],
10940
+ ["path", {
10941
+ d: "m19.07 10.93-1.41 1.41",
10942
+ key: "15zs5n"
10943
+ }],
10944
+ ["path", {
10945
+ d: "M22 22H2",
10946
+ key: "19qnx5"
10947
+ }],
10948
+ ["path", {
10949
+ d: "m8 6 4-4 4 4",
10950
+ key: "ybng9g"
10951
+ }],
10952
+ ["path", {
10953
+ d: "M16 18a4 4 0 0 0-8 0",
10954
+ key: "1lzouq"
10955
+ }]
10956
+ ]);
10957
+ /**
10958
+ * @license lucide-react v0.576.0 - ISC
10959
+ *
10960
+ * This source code is licensed under the ISC license.
10961
+ * See the LICENSE file in the root directory of this source tree.
10962
+ */
10857
10963
  var Table2 = createLucideIcon("table-2", [["path", {
10858
10964
  d: "M9 3H5a2 2 0 0 0-2 2v4m6-6h10a2 2 0 0 1 2 2v4M9 3v18m0 0h10a2 2 0 0 0 2-2V9M9 21H5a2 2 0 0 1-2-2V9m0 0h18",
10859
10965
  key: "gugj83"
@@ -14673,6 +14779,26 @@ var DEVICE_ROLE_META = {
14673
14779
  icon: Gauge,
14674
14780
  label: "Pressure"
14675
14781
  },
14782
+ [_camstack_types.DeviceRole.WindSensor]: {
14783
+ icon: Wind,
14784
+ label: "Wind"
14785
+ },
14786
+ [_camstack_types.DeviceRole.RainSensor]: {
14787
+ icon: CloudRain,
14788
+ label: "Rain"
14789
+ },
14790
+ [_camstack_types.DeviceRole.UvSensor]: {
14791
+ icon: SunMedium,
14792
+ label: "UV index"
14793
+ },
14794
+ [_camstack_types.DeviceRole.SolarRadiationSensor]: {
14795
+ icon: Sunrise,
14796
+ label: "Solar radiation"
14797
+ },
14798
+ [_camstack_types.DeviceRole.SoilMoistureSensor]: {
14799
+ icon: Sprout,
14800
+ label: "Soil moisture"
14801
+ },
14676
14802
  [_camstack_types.DeviceRole.PowerSensor]: {
14677
14803
  icon: Zap,
14678
14804
  label: "Power"
@@ -17084,6 +17210,8 @@ var useDeviceManagerAdoptionRefresh = trpc.deviceManager.adoptionRefresh.useMuta
17084
17210
  var useDeviceManagerAdoptionAdopt = trpc.deviceManager.adoptionAdopt.useMutation;
17085
17211
  /** Generated alias around `trpc.deviceManager.adoptionRelease.useMutation`. */
17086
17212
  var useDeviceManagerAdoptionRelease = trpc.deviceManager.adoptionRelease.useMutation;
17213
+ /** Generated alias around `trpc.deviceManager.adoptionResync.useMutation`. */
17214
+ var useDeviceManagerAdoptionResync = trpc.deviceManager.adoptionResync.useMutation;
17087
17215
  /** Generated alias around `trpc.deviceManager.testField.useMutation`. */
17088
17216
  var useDeviceManagerTestField = trpc.deviceManager.testField.useMutation;
17089
17217
  /** Generated alias around `trpc.deviceManager.getDeviceStatusAggregate.useQuery`. */
@@ -18131,6 +18259,31 @@ var ROLE_DESCRIPTOR = {
18131
18259
  field: "value",
18132
18260
  getSliceHandle: (s) => s.numericSensor
18133
18261
  },
18262
+ "wind-sensor": {
18263
+ kind: "numeric",
18264
+ field: "value",
18265
+ getSliceHandle: (s) => s.numericSensor
18266
+ },
18267
+ "rain-sensor": {
18268
+ kind: "numeric",
18269
+ field: "value",
18270
+ getSliceHandle: (s) => s.numericSensor
18271
+ },
18272
+ "uv-sensor": {
18273
+ kind: "numeric",
18274
+ field: "value",
18275
+ getSliceHandle: (s) => s.numericSensor
18276
+ },
18277
+ "solar-radiation-sensor": {
18278
+ kind: "numeric",
18279
+ field: "value",
18280
+ getSliceHandle: (s) => s.numericSensor
18281
+ },
18282
+ "soil-moisture-sensor": {
18283
+ kind: "numeric",
18284
+ field: "value",
18285
+ getSliceHandle: (s) => s.numericSensor
18286
+ },
18134
18287
  "power-sensor": {
18135
18288
  kind: "numeric",
18136
18289
  field: "watts",
@@ -18644,6 +18797,10 @@ function vacuumStateMeta(state) {
18644
18797
  label: "Docked",
18645
18798
  tone: "text-success"
18646
18799
  };
18800
+ case "drying": return {
18801
+ label: "Drying",
18802
+ tone: "text-warning"
18803
+ };
18647
18804
  case "idle": return {
18648
18805
  label: "Idle",
18649
18806
  tone: "text-foreground-subtle"
@@ -18700,6 +18857,12 @@ function vacuumGraphicTint(state) {
18700
18857
  docked: true,
18701
18858
  active: false
18702
18859
  };
18860
+ case "drying": return {
18861
+ glow: "bg-warning/10",
18862
+ accent: "text-warning",
18863
+ docked: true,
18864
+ active: false
18865
+ };
18703
18866
  case "paused": return {
18704
18867
  glow: "bg-warning/10",
18705
18868
  accent: "text-warning",
@@ -18870,8 +19033,10 @@ function tankMeta(kind) {
18870
19033
  /**
18871
19034
  * TankIndicator — one tank row: label + glyph, then a fill bar (numeric
18872
19035
  * level) or a status pill (discrete), tinted by `tankAlert`. Renders
18873
- * nothing when the tank is absent (null) or its reading is unknown
18874
- * (both fields null).
19036
+ * nothing only when the tank is absent (null). A PRESENT tank whose
19037
+ * reading is unknown (both fields null) still renders a neutral row with
19038
+ * an em-dash placeholder, so the user always sees which tanks the robot
19039
+ * has (Dreame reports presence but no fill telemetry).
18875
19040
  */
18876
19041
  function TankIndicator({ kind, tank }) {
18877
19042
  if (tank === null) return null;
@@ -18907,15 +19072,22 @@ function TankIndicator({ kind, tank }) {
18907
19072
  children: tank.status
18908
19073
  })]
18909
19074
  });
18910
- return null;
19075
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
19076
+ className: "flex items-center gap-2 text-xs",
19077
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
19078
+ className: "inline-flex items-center gap-1 w-24 shrink-0 text-foreground-subtle",
19079
+ children: [meta.icon, meta.label]
19080
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
19081
+ className: "inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-medium bg-surface-hover text-foreground-subtle",
19082
+ children: "—"
19083
+ })]
19084
+ });
18911
19085
  }
18912
19086
  /**
18913
19087
  * TanksRow — compact 2-column grid of the present tanks for the vacuum
18914
- * hero. Absent tanks (null) are skipped; the row renders nothing unless at
18915
- * least one tank is actually renderablepresent AND with a known reading
18916
- * (level or status). A tank that is present but all-unknown
18917
- * (`{level:null,status:null}`) renders nothing, so a hero of only such
18918
- * tanks would otherwise show an empty bordered box.
19088
+ * hero. Absent tanks (null) are skipped; every PRESENT tank is shown, even
19089
+ * when its reading is unknown (`{level:null,status:null}`)it renders a
19090
+ * neutral em-dash row. The row renders iff the robot has at least one tank.
18919
19091
  */
18920
19092
  function TanksRow({ cleanWater, dirtyWater, detergent, dustBin }) {
18921
19093
  const tanks = [
@@ -18924,7 +19096,7 @@ function TanksRow({ cleanWater, dirtyWater, detergent, dustBin }) {
18924
19096
  ["detergent", detergent],
18925
19097
  ["dustBin", dustBin]
18926
19098
  ];
18927
- if (!tanks.some(([, t]) => t !== null && (t.level !== null || t.status !== null))) return null;
19099
+ if (!tanks.some(([, t]) => t !== null)) return null;
18928
19100
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
18929
19101
  className: "grid grid-cols-1 gap-x-6 gap-y-2 rounded-md border border-border bg-surface-hover/40 p-3 sm:grid-cols-2",
18930
19102
  children: tanks.map(([kind, tank]) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TankIndicator, {
@@ -19018,52 +19190,72 @@ function VacuumHeroCard({ trpc, deviceId }) {
19018
19190
  className: "flex items-center gap-6",
19019
19191
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(VacuumGraphic, { state }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
19020
19192
  className: "flex flex-col gap-3 flex-1",
19021
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
19022
- className: "flex items-center justify-between",
19023
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
19024
- className: cn("text-3xl font-semibold", meta.tone),
19025
- children: meta.label
19026
- }), slice?.batteryLevel != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BatteryBadge$2, { level: slice.batteryLevel })]
19027
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
19028
- className: "flex items-center gap-2",
19029
- children: [
19030
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VacuumHeroButton, {
19031
- label: "Start",
19032
- busy,
19033
- active: state === "cleaning",
19034
- onClick: () => press(cap.start),
19035
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Play, { className: "h-5 w-5" })
19036
- }),
19037
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VacuumHeroButton, {
19038
- label: "Pause",
19039
- busy,
19040
- active: state === "paused",
19041
- onClick: () => press(cap.pause),
19042
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pause, { className: "h-5 w-5" })
19043
- }),
19044
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VacuumHeroButton, {
19045
- label: "Stop",
19046
- busy,
19047
- active: false,
19048
- onClick: () => press(cap.stop),
19049
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Square, { className: "h-4 w-4" })
19050
- }),
19051
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VacuumHeroButton, {
19052
- label: "Return to base",
19053
- busy,
19054
- active: state === "returning",
19055
- onClick: () => press(cap.returnToBase),
19056
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(House, { className: "h-5 w-5" })
19057
- }),
19058
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VacuumHeroButton, {
19059
- label: "Locate",
19060
- busy,
19061
- active: false,
19062
- onClick: () => press(cap.locate),
19063
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Megaphone, { className: "h-5 w-5" })
19064
- })
19065
- ]
19066
- })]
19193
+ children: [
19194
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
19195
+ className: "flex items-center justify-between",
19196
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
19197
+ className: cn("text-3xl font-semibold", meta.tone),
19198
+ children: meta.label
19199
+ }), slice?.batteryLevel != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BatteryBadge$2, { level: slice.batteryLevel })]
19200
+ }),
19201
+ slice?.progressPercent != null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
19202
+ className: "flex items-center gap-2",
19203
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
19204
+ className: "h-1.5 flex-1 rounded bg-surface-hover overflow-hidden",
19205
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
19206
+ className: "h-full bg-primary transition-[width] duration-500",
19207
+ style: { width: `${Math.max(0, Math.min(100, slice.progressPercent))}%` }
19208
+ })
19209
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
19210
+ className: "tabular-nums text-xs text-foreground-subtle",
19211
+ children: [Math.round(slice.progressPercent), "%"]
19212
+ })]
19213
+ }),
19214
+ slice?.errorCode != null && slice.errorCode !== 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
19215
+ className: "inline-flex items-center gap-1 text-xs text-destructive",
19216
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TriangleAlert, { className: "h-3.5 w-3.5" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: slice.errorLabel ?? `Error ${slice.errorCode}` })]
19217
+ }),
19218
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
19219
+ className: "flex items-center gap-2",
19220
+ children: [
19221
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VacuumHeroButton, {
19222
+ label: "Start",
19223
+ busy,
19224
+ active: state === "cleaning",
19225
+ onClick: () => press(cap.start),
19226
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Play, { className: "h-5 w-5" })
19227
+ }),
19228
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VacuumHeroButton, {
19229
+ label: "Pause",
19230
+ busy,
19231
+ active: state === "paused",
19232
+ onClick: () => press(cap.pause),
19233
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pause, { className: "h-5 w-5" })
19234
+ }),
19235
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VacuumHeroButton, {
19236
+ label: "Stop",
19237
+ busy,
19238
+ active: false,
19239
+ onClick: () => press(cap.stop),
19240
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Square, { className: "h-4 w-4" })
19241
+ }),
19242
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VacuumHeroButton, {
19243
+ label: "Return to base",
19244
+ busy,
19245
+ active: state === "returning",
19246
+ onClick: () => press(cap.returnToBase),
19247
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(House, { className: "h-5 w-5" })
19248
+ }),
19249
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VacuumHeroButton, {
19250
+ label: "Locate",
19251
+ busy,
19252
+ active: false,
19253
+ onClick: () => press(cap.locate),
19254
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Megaphone, { className: "h-5 w-5" })
19255
+ })
19256
+ ]
19257
+ })
19258
+ ]
19067
19259
  })]
19068
19260
  }),
19069
19261
  fanSpeeds.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
@@ -19291,6 +19483,37 @@ function LawnMowerGraphic({ activity }) {
19291
19483
  })]
19292
19484
  });
19293
19485
  }
19486
+ /** Tailwind text tone for a device-code severity. */
19487
+ function severityTone(severity) {
19488
+ switch (severity) {
19489
+ case "error": return "text-destructive";
19490
+ case "warning": return "text-warning";
19491
+ case "info": return "text-foreground-subtle";
19492
+ }
19493
+ }
19494
+ /** Mowing-progress bar + % — shown only when a progress value is present. */
19495
+ function ProgressBar({ percent }) {
19496
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
19497
+ className: "flex items-center gap-2",
19498
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
19499
+ className: "h-1.5 flex-1 rounded bg-surface-hover overflow-hidden",
19500
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
19501
+ className: "h-full bg-primary transition-[width] duration-500",
19502
+ style: { width: `${Math.max(0, Math.min(100, percent))}%` }
19503
+ })
19504
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
19505
+ className: "tabular-nums text-xs text-foreground-subtle",
19506
+ children: [Math.round(percent), "%"]
19507
+ })]
19508
+ });
19509
+ }
19510
+ /** Current device/event code line — tinted by severity; an icon for warn/error. */
19511
+ function CodeLine({ code, label, severity }) {
19512
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
19513
+ className: cn("inline-flex items-center gap-1 text-xs", severityTone(severity)),
19514
+ children: [severity !== "info" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TriangleAlert, { className: "h-3.5 w-3.5" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label ?? `Code ${code}` })]
19515
+ });
19516
+ }
19294
19517
  /** Battery % badge — shared between inline + hero. */
19295
19518
  function BatteryBadge$1({ level }) {
19296
19519
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
@@ -19394,38 +19617,47 @@ function LawnMowerHeroCard({ trpc, deviceId }) {
19394
19617
  className: "flex items-center gap-6",
19395
19618
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LawnMowerGraphic, { activity }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
19396
19619
  className: "flex flex-col gap-3 flex-1",
19397
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
19398
- className: "flex items-center justify-between",
19399
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
19400
- className: cn("text-3xl font-semibold", meta.tone),
19401
- children: meta.label
19402
- }), slice?.batteryLevel != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BatteryBadge$1, { level: slice.batteryLevel })]
19403
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
19404
- className: "flex items-center gap-2",
19405
- children: [
19406
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LawnMowerHeroButton, {
19407
- label: "Start mowing",
19408
- busy,
19409
- active: activity === "mowing",
19410
- onClick: () => press(cap.startMowing),
19411
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Play, { className: "h-5 w-5" })
19412
- }),
19413
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LawnMowerHeroButton, {
19414
- label: "Pause",
19415
- busy,
19416
- active: activity === "paused",
19417
- onClick: () => press(cap.pause),
19418
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pause, { className: "h-5 w-5" })
19419
- }),
19420
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LawnMowerHeroButton, {
19421
- label: "Dock",
19422
- busy,
19423
- active: activity === "docked",
19424
- onClick: () => press(cap.dock),
19425
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(House, { className: "h-5 w-5" })
19426
- })
19427
- ]
19428
- })]
19620
+ children: [
19621
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
19622
+ className: "flex items-center justify-between",
19623
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
19624
+ className: cn("text-3xl font-semibold", meta.tone),
19625
+ children: meta.label
19626
+ }), slice?.batteryLevel != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BatteryBadge$1, { level: slice.batteryLevel })]
19627
+ }),
19628
+ slice?.progressPercent != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProgressBar, { percent: slice.progressPercent }),
19629
+ slice?.currentCode != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CodeLine, {
19630
+ code: slice.currentCode,
19631
+ label: slice.currentCodeLabel,
19632
+ severity: slice.severity
19633
+ }),
19634
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
19635
+ className: "flex items-center gap-2",
19636
+ children: [
19637
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LawnMowerHeroButton, {
19638
+ label: "Start mowing",
19639
+ busy,
19640
+ active: activity === "mowing",
19641
+ onClick: () => press(cap.startMowing),
19642
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Play, { className: "h-5 w-5" })
19643
+ }),
19644
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LawnMowerHeroButton, {
19645
+ label: "Pause",
19646
+ busy,
19647
+ active: activity === "paused",
19648
+ onClick: () => press(cap.pause),
19649
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pause, { className: "h-5 w-5" })
19650
+ }),
19651
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LawnMowerHeroButton, {
19652
+ label: "Dock",
19653
+ busy,
19654
+ active: activity === "docked",
19655
+ onClick: () => press(cap.dock),
19656
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(House, { className: "h-5 w-5" })
19657
+ })
19658
+ ]
19659
+ })
19660
+ ]
19429
19661
  })]
19430
19662
  })
19431
19663
  });
@@ -21159,103 +21391,6 @@ function WeatherHeroCard({ trpc, deviceId }) {
21159
21391
  });
21160
21392
  }
21161
21393
  //#endregion
21162
- //#region src/composites/hover-zoom-image.tsx
21163
- /**
21164
- * <HoverZoomImage> — a small thumbnail that expands into a floating
21165
- * enlarged preview on hover/focus.
21166
- *
21167
- * One responsibility: render a compact thumbnail (image when `src` is
21168
- * present, a muted `ImageOff` placeholder box when null) and, while the
21169
- * thumbnail is hovered or focused, portal a larger preview to
21170
- * `document.body` positioned with FLIP-ON-COLLISION:
21171
- * - the position is computed from the thumbnail's `getBoundingClientRect`;
21172
- * - placed ABOVE the thumbnail unless there is insufficient space (then
21173
- * below); left-clamped to the viewport;
21174
- * - `position:fixed`, `pointer-events:none`, `z-[9999]`.
21175
- *
21176
- * The portal is what lets this work inside the device grid, whose overflow
21177
- * ancestors would otherwise clip a normally-positioned popover.
21178
- *
21179
- * Reused by BOTH the Image DeviceType inline control (signed image url) and
21180
- * the camera-snapshot cell in device rows (snapshot blob/object url).
21181
- */
21182
- var DEFAULT_EXPANDED_WIDTH = 288;
21183
- var DEFAULT_EXPANDED_HEIGHT = 196;
21184
- var DEFAULT_THUMB_CLASS = "h-7 w-7 rounded";
21185
- var TRIGGER_GAP = 8;
21186
- var VIEWPORT_MARGIN = 8;
21187
- function HoverZoomImage({ src, alt, caption, placeholder = "No image", thumbClassName = DEFAULT_THUMB_CLASS, expandedWidth = DEFAULT_EXPANDED_WIDTH, expandedHeight = DEFAULT_EXPANDED_HEIGHT }) {
21188
- const triggerRef = (0, react$1.useRef)(null);
21189
- const [position, setPosition] = (0, react$1.useState)(null);
21190
- const [errored, setErrored] = (0, react$1.useState)(false);
21191
- (0, react$1.useEffect)(() => {
21192
- setErrored(false);
21193
- }, [src]);
21194
- const hasImage = src !== null && !errored;
21195
- const safeSrc = src ?? void 0;
21196
- const open = (0, react$1.useCallback)(() => {
21197
- const el = triggerRef.current;
21198
- if (!el) return;
21199
- const rect = el.getBoundingClientRect();
21200
- const spaceAbove = rect.top;
21201
- const spaceBelow = window.innerHeight - rect.bottom;
21202
- const top = spaceAbove < expandedHeight + TRIGGER_GAP && spaceBelow > spaceAbove ? rect.bottom + TRIGGER_GAP : rect.top - expandedHeight - TRIGGER_GAP;
21203
- const desiredLeft = rect.right - expandedWidth;
21204
- setPosition({
21205
- top,
21206
- left: Math.max(VIEWPORT_MARGIN, Math.min(desiredLeft, window.innerWidth - expandedWidth - VIEWPORT_MARGIN))
21207
- });
21208
- }, [expandedWidth, expandedHeight]);
21209
- const close = (0, react$1.useCallback)(() => setPosition(null), []);
21210
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
21211
- ref: triggerRef,
21212
- type: "button",
21213
- onClick: (e) => e.stopPropagation(),
21214
- onMouseEnter: open,
21215
- onMouseLeave: close,
21216
- onFocus: open,
21217
- onBlur: close,
21218
- "aria-label": alt,
21219
- className: `flex flex-shrink-0 items-center justify-center overflow-hidden border border-border bg-background text-foreground-subtle transition-colors hover:bg-surface-hover ${thumbClassName}`,
21220
- children: hasImage ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
21221
- src: safeSrc,
21222
- alt: "",
21223
- onError: () => setErrored(true),
21224
- className: "h-full w-full object-cover"
21225
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageOff, {
21226
- className: "h-3.5 w-3.5",
21227
- "aria-hidden": "true"
21228
- })
21229
- }), position && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
21230
- "aria-hidden": "true",
21231
- style: {
21232
- position: "fixed",
21233
- top: position.top,
21234
- left: position.left,
21235
- width: expandedWidth,
21236
- pointerEvents: "none"
21237
- },
21238
- className: "z-[9999] overflow-hidden rounded-lg border border-border bg-surface shadow-lg shadow-black/30",
21239
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
21240
- className: "flex h-40 items-center justify-center bg-background",
21241
- children: hasImage ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
21242
- src: safeSrc,
21243
- alt,
21244
- className: "h-full w-full object-cover"
21245
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
21246
- className: "text-xs text-foreground-subtle",
21247
- children: placeholder
21248
- })
21249
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
21250
- className: "px-3 py-2",
21251
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
21252
- className: "truncate text-xs font-semibold text-foreground",
21253
- children: caption ?? alt
21254
- })
21255
- })]
21256
- }), document.body)] });
21257
- }
21258
- //#endregion
21259
21394
  //#region src/composites/device-controls/image-control.tsx
21260
21395
  /**
21261
21396
  * Image controls for a `DeviceType.Image` device.
@@ -21279,6 +21414,91 @@ function useImage(trpc, deviceId) {
21279
21414
  slice: (d) => d.state.image
21280
21415
  });
21281
21416
  }
21417
+ var ZOOM_MIN = 1;
21418
+ var ZOOM_MAX = 6;
21419
+ var ZOOM_STEP = .5;
21420
+ /**
21421
+ * ImageDetailModal — a full-view image dialog with zoom in/out/reset and pan
21422
+ * (scroll when zoomed). Generous padding frames the image. Opened by clicking
21423
+ * the inline thumbnail or the hero image. Controlled via `open`/`onOpenChange`.
21424
+ */
21425
+ function ImageDetailModal({ url, alt, open, onOpenChange }) {
21426
+ const [scale, setScale] = (0, react$1.useState)(1);
21427
+ (0, react$1.useEffect)(() => {
21428
+ if (open) setScale(1);
21429
+ }, [open, url]);
21430
+ const zoomIn = (0, react$1.useCallback)(() => setScale((s) => Math.min(ZOOM_MAX, s + ZOOM_STEP)), []);
21431
+ const zoomOut = (0, react$1.useCallback)(() => setScale((s) => Math.max(ZOOM_MIN, s - ZOOM_STEP)), []);
21432
+ const reset = (0, react$1.useCallback)(() => setScale(1), []);
21433
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Dialog, {
21434
+ open,
21435
+ onOpenChange,
21436
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DialogContent, {
21437
+ width: "lg",
21438
+ className: "max-w-5xl p-6",
21439
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
21440
+ className: "flex items-center justify-between gap-2 mb-3",
21441
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
21442
+ className: "truncate text-sm font-semibold text-foreground",
21443
+ children: alt
21444
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
21445
+ className: "flex items-center gap-1",
21446
+ children: [
21447
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModalIconButton, {
21448
+ label: "Zoom out",
21449
+ disabled: scale <= ZOOM_MIN,
21450
+ onClick: zoomOut,
21451
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZoomOut, { className: "h-4 w-4" })
21452
+ }),
21453
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
21454
+ className: "tabular-nums text-xs text-foreground-subtle w-10 text-center",
21455
+ children: [Math.round(scale * 100), "%"]
21456
+ }),
21457
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModalIconButton, {
21458
+ label: "Zoom in",
21459
+ disabled: scale >= ZOOM_MAX,
21460
+ onClick: zoomIn,
21461
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZoomIn, { className: "h-4 w-4" })
21462
+ }),
21463
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModalIconButton, {
21464
+ label: "Reset zoom",
21465
+ disabled: scale === 1,
21466
+ onClick: reset,
21467
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RotateCcw, { className: "h-4 w-4" })
21468
+ }),
21469
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModalIconButton, {
21470
+ label: "Close",
21471
+ onClick: () => onOpenChange(false),
21472
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(X, { className: "h-4 w-4" })
21473
+ })
21474
+ ]
21475
+ })]
21476
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
21477
+ className: "flex max-h-[75vh] items-center justify-center overflow-auto rounded border border-border bg-gradient-to-b from-surface-hover to-surface p-6",
21478
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
21479
+ src: url,
21480
+ alt,
21481
+ style: {
21482
+ transform: `scale(${scale})`,
21483
+ transformOrigin: "center"
21484
+ },
21485
+ className: "max-h-[65vh] w-auto object-contain transition-transform duration-150"
21486
+ })
21487
+ })]
21488
+ })
21489
+ });
21490
+ }
21491
+ function ModalIconButton({ label, disabled, onClick, children }) {
21492
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
21493
+ type: "button",
21494
+ "aria-label": label,
21495
+ title: label,
21496
+ disabled,
21497
+ onClick,
21498
+ className: "inline-flex h-7 w-7 items-center justify-center rounded text-foreground-subtle hover:text-foreground hover:bg-surface-hover transition-colors disabled:opacity-40 disabled:cursor-not-allowed",
21499
+ children
21500
+ });
21501
+ }
21282
21502
  /**
21283
21503
  * ImageInlineControl — compact thumbnail for a device-list row that expands
21284
21504
  * into a floating preview on hover/focus (shared `HoverZoomImage`). Shows a
@@ -21289,13 +21509,36 @@ function useImage(trpc, deviceId) {
21289
21509
  */
21290
21510
  function ImageInlineControl({ trpc, deviceId }) {
21291
21511
  const { slice } = useImage(trpc, deviceId);
21512
+ const [open, setOpen] = (0, react$1.useState)(false);
21292
21513
  if (slice === void 0) return null;
21293
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HoverZoomImage, {
21294
- src: slice.url,
21295
- alt: "Image",
21296
- placeholder: "No image",
21297
- thumbClassName: "h-7 w-7 rounded"
21514
+ const url = slice.url;
21515
+ if (url === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
21516
+ className: "inline-flex h-7 w-7 items-center justify-center rounded bg-surface-hover text-foreground-subtle",
21517
+ title: "No image",
21518
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageOff, {
21519
+ className: "h-4 w-4",
21520
+ "aria-hidden": "true"
21521
+ })
21298
21522
  });
21523
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
21524
+ type: "button",
21525
+ title: "View image",
21526
+ onClick: (e) => {
21527
+ e.stopPropagation();
21528
+ setOpen(true);
21529
+ },
21530
+ className: "inline-flex items-center justify-center rounded ring-offset-1 hover:ring-2 hover:ring-primary/50 transition",
21531
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
21532
+ src: url,
21533
+ alt: "Image",
21534
+ className: "h-7 w-7 rounded object-cover"
21535
+ })
21536
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageDetailModal, {
21537
+ url,
21538
+ alt: "Image",
21539
+ open,
21540
+ onOpenChange: setOpen
21541
+ })] });
21299
21542
  }
21300
21543
  /**
21301
21544
  * ImageHeroCard — read-only framed image for the device-detail hero.
@@ -21308,19 +21551,27 @@ function ImageInlineControl({ trpc, deviceId }) {
21308
21551
  function ImageHeroCard({ trpc, deviceId, deviceName }) {
21309
21552
  const { slice } = useImage(trpc, deviceId);
21310
21553
  const [errored, setErrored] = (0, react$1.useState)(false);
21554
+ const [open, setOpen] = (0, react$1.useState)(false);
21555
+ const url = slice?.url ?? null;
21311
21556
  (0, react$1.useEffect)(() => {
21312
21557
  setErrored(false);
21313
- }, [slice?.url ?? null]);
21558
+ }, [url]);
21314
21559
  if (slice === void 0) return null;
21315
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
21560
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
21316
21561
  className: "flex flex-col items-stretch gap-3 rounded-lg border border-border bg-surface p-4",
21317
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
21562
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
21318
21563
  className: "flex w-full items-center justify-center overflow-hidden rounded border-2 border-border bg-gradient-to-b from-surface-hover to-surface",
21319
- children: slice.url !== null && !errored ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
21320
- src: slice.url,
21321
- alt: deviceName ?? "Image",
21322
- onError: () => setErrored(true),
21323
- className: "max-h-72 w-full object-contain"
21564
+ children: slice.url !== null && !errored ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
21565
+ type: "button",
21566
+ title: "View image",
21567
+ onClick: () => setOpen(true),
21568
+ className: "flex w-full cursor-zoom-in items-center justify-center",
21569
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
21570
+ src: slice.url,
21571
+ alt: deviceName ?? "Image",
21572
+ onError: () => setErrored(true),
21573
+ className: "max-h-72 w-full object-contain"
21574
+ })
21324
21575
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
21325
21576
  "data-testid": "image-placeholder",
21326
21577
  className: "flex h-48 w-full flex-col items-center justify-center gap-2 text-foreground-subtle",
@@ -21332,7 +21583,12 @@ function ImageHeroCard({ trpc, deviceId, deviceName }) {
21332
21583
  children: "No image"
21333
21584
  })]
21334
21585
  })
21335
- })
21586
+ }), url !== null && !errored && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageDetailModal, {
21587
+ url,
21588
+ alt: deviceName ?? "Image",
21589
+ open,
21590
+ onOpenChange: setOpen
21591
+ })]
21336
21592
  });
21337
21593
  }
21338
21594
  //#endregion
@@ -24891,6 +25147,103 @@ function BatteryBadge({ status, variant = "compact", className }) {
24891
25147
  });
24892
25148
  }
24893
25149
  //#endregion
25150
+ //#region src/composites/hover-zoom-image.tsx
25151
+ /**
25152
+ * <HoverZoomImage> — a small thumbnail that expands into a floating
25153
+ * enlarged preview on hover/focus.
25154
+ *
25155
+ * One responsibility: render a compact thumbnail (image when `src` is
25156
+ * present, a muted `ImageOff` placeholder box when null) and, while the
25157
+ * thumbnail is hovered or focused, portal a larger preview to
25158
+ * `document.body` positioned with FLIP-ON-COLLISION:
25159
+ * - the position is computed from the thumbnail's `getBoundingClientRect`;
25160
+ * - placed ABOVE the thumbnail unless there is insufficient space (then
25161
+ * below); left-clamped to the viewport;
25162
+ * - `position:fixed`, `pointer-events:none`, `z-[9999]`.
25163
+ *
25164
+ * The portal is what lets this work inside the device grid, whose overflow
25165
+ * ancestors would otherwise clip a normally-positioned popover.
25166
+ *
25167
+ * Reused by BOTH the Image DeviceType inline control (signed image url) and
25168
+ * the camera-snapshot cell in device rows (snapshot blob/object url).
25169
+ */
25170
+ var DEFAULT_EXPANDED_WIDTH = 288;
25171
+ var DEFAULT_EXPANDED_HEIGHT = 196;
25172
+ var DEFAULT_THUMB_CLASS = "h-7 w-7 rounded";
25173
+ var TRIGGER_GAP = 8;
25174
+ var VIEWPORT_MARGIN = 8;
25175
+ function HoverZoomImage({ src, alt, caption, placeholder = "No image", thumbClassName = DEFAULT_THUMB_CLASS, expandedWidth = DEFAULT_EXPANDED_WIDTH, expandedHeight = DEFAULT_EXPANDED_HEIGHT }) {
25176
+ const triggerRef = (0, react$1.useRef)(null);
25177
+ const [position, setPosition] = (0, react$1.useState)(null);
25178
+ const [errored, setErrored] = (0, react$1.useState)(false);
25179
+ (0, react$1.useEffect)(() => {
25180
+ setErrored(false);
25181
+ }, [src]);
25182
+ const hasImage = src !== null && !errored;
25183
+ const safeSrc = src ?? void 0;
25184
+ const open = (0, react$1.useCallback)(() => {
25185
+ const el = triggerRef.current;
25186
+ if (!el) return;
25187
+ const rect = el.getBoundingClientRect();
25188
+ const spaceAbove = rect.top;
25189
+ const spaceBelow = window.innerHeight - rect.bottom;
25190
+ const top = spaceAbove < expandedHeight + TRIGGER_GAP && spaceBelow > spaceAbove ? rect.bottom + TRIGGER_GAP : rect.top - expandedHeight - TRIGGER_GAP;
25191
+ const desiredLeft = rect.right - expandedWidth;
25192
+ setPosition({
25193
+ top,
25194
+ left: Math.max(VIEWPORT_MARGIN, Math.min(desiredLeft, window.innerWidth - expandedWidth - VIEWPORT_MARGIN))
25195
+ });
25196
+ }, [expandedWidth, expandedHeight]);
25197
+ const close = (0, react$1.useCallback)(() => setPosition(null), []);
25198
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
25199
+ ref: triggerRef,
25200
+ type: "button",
25201
+ onClick: (e) => e.stopPropagation(),
25202
+ onMouseEnter: open,
25203
+ onMouseLeave: close,
25204
+ onFocus: open,
25205
+ onBlur: close,
25206
+ "aria-label": alt,
25207
+ className: `flex flex-shrink-0 items-center justify-center overflow-hidden border border-border bg-background text-foreground-subtle transition-colors hover:bg-surface-hover ${thumbClassName}`,
25208
+ children: hasImage ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
25209
+ src: safeSrc,
25210
+ alt: "",
25211
+ onError: () => setErrored(true),
25212
+ className: "h-full w-full object-cover"
25213
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageOff, {
25214
+ className: "h-3.5 w-3.5",
25215
+ "aria-hidden": "true"
25216
+ })
25217
+ }), position && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
25218
+ "aria-hidden": "true",
25219
+ style: {
25220
+ position: "fixed",
25221
+ top: position.top,
25222
+ left: position.left,
25223
+ width: expandedWidth,
25224
+ pointerEvents: "none"
25225
+ },
25226
+ className: "z-[9999] overflow-hidden rounded-lg border border-border bg-surface shadow-lg shadow-black/30",
25227
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
25228
+ className: "flex h-40 items-center justify-center bg-background",
25229
+ children: hasImage ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
25230
+ src: safeSrc,
25231
+ alt,
25232
+ className: "h-full w-full object-cover"
25233
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
25234
+ className: "text-xs text-foreground-subtle",
25235
+ children: placeholder
25236
+ })
25237
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
25238
+ className: "px-3 py-2",
25239
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
25240
+ className: "truncate text-xs font-semibold text-foreground",
25241
+ children: caption ?? alt
25242
+ })
25243
+ })]
25244
+ }), document.body)] });
25245
+ }
25246
+ //#endregion
24894
25247
  //#region src/composites/device-item/preview.tsx
24895
25248
  function DeviceItemPreview({ trpc, device, status, enabled, showStatusPills = false }) {
24896
25249
  const snapshot = useDeviceSnapshotImage(trpc, enabled && device.online ? device.id : null);
@@ -42721,6 +43074,7 @@ exports.useDeviceManagerAdoptionAdopt = useDeviceManagerAdoptionAdopt;
42721
43074
  exports.useDeviceManagerAdoptionListCandidates = useDeviceManagerAdoptionListCandidates;
42722
43075
  exports.useDeviceManagerAdoptionRefresh = useDeviceManagerAdoptionRefresh;
42723
43076
  exports.useDeviceManagerAdoptionRelease = useDeviceManagerAdoptionRelease;
43077
+ exports.useDeviceManagerAdoptionResync = useDeviceManagerAdoptionResync;
42724
43078
  exports.useDeviceManagerAllocateDeviceId = useDeviceManagerAllocateDeviceId;
42725
43079
  exports.useDeviceManagerApplyInitialMeta = useDeviceManagerApplyInitialMeta;
42726
43080
  exports.useDeviceManagerCreateDevice = useDeviceManagerCreateDevice;