@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/composites/device-controls/image-control.d.ts +11 -0
- package/dist/generated/system-hooks.d.ts +2 -0
- package/dist/index.cjs +552 -198
- package/dist/index.js +552 -199
- package/package.json +1 -1
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)
|
|
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
|
|
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;
|
|
18915
|
-
*
|
|
18916
|
-
*
|
|
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
|
|
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: [
|
|
19022
|
-
|
|
19023
|
-
|
|
19024
|
-
|
|
19025
|
-
|
|
19026
|
-
|
|
19027
|
-
|
|
19028
|
-
|
|
19029
|
-
|
|
19030
|
-
|
|
19031
|
-
|
|
19032
|
-
|
|
19033
|
-
|
|
19034
|
-
|
|
19035
|
-
|
|
19036
|
-
|
|
19037
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
19038
|
-
|
|
19039
|
-
|
|
19040
|
-
|
|
19041
|
-
|
|
19042
|
-
|
|
19043
|
-
|
|
19044
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
19045
|
-
|
|
19046
|
-
|
|
19047
|
-
|
|
19048
|
-
|
|
19049
|
-
|
|
19050
|
-
|
|
19051
|
-
|
|
19052
|
-
|
|
19053
|
-
|
|
19054
|
-
|
|
19055
|
-
|
|
19056
|
-
|
|
19057
|
-
|
|
19058
|
-
|
|
19059
|
-
|
|
19060
|
-
|
|
19061
|
-
|
|
19062
|
-
|
|
19063
|
-
|
|
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: [
|
|
19398
|
-
|
|
19399
|
-
|
|
19400
|
-
|
|
19401
|
-
|
|
19402
|
-
|
|
19403
|
-
|
|
19404
|
-
|
|
19405
|
-
|
|
19406
|
-
|
|
19407
|
-
|
|
19408
|
-
|
|
19409
|
-
|
|
19410
|
-
|
|
19411
|
-
|
|
19412
|
-
|
|
19413
|
-
|
|
19414
|
-
|
|
19415
|
-
|
|
19416
|
-
|
|
19417
|
-
|
|
19418
|
-
|
|
19419
|
-
|
|
19420
|
-
|
|
19421
|
-
|
|
19422
|
-
|
|
19423
|
-
|
|
19424
|
-
|
|
19425
|
-
|
|
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
|
-
|
|
21294
|
-
|
|
21295
|
-
|
|
21296
|
-
|
|
21297
|
-
|
|
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
|
-
}, [
|
|
21558
|
+
}, [url]);
|
|
21314
21559
|
if (slice === void 0) return null;
|
|
21315
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.
|
|
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)("
|
|
21320
|
-
|
|
21321
|
-
|
|
21322
|
-
|
|
21323
|
-
className: "
|
|
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;
|