diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json
index 391b235b9..681166967 100644
--- a/packages/translation/src/lang/en.json
+++ b/packages/translation/src/lang/en.json
@@ -1905,6 +1905,16 @@
"customDateFormat": {
"label": "Custom date format",
"description": "Use ISO 8601 to format date (this will override other options)"
+ },
+ "showWeather": {
+ "label": "Show weather",
+ "description": "Display a small animated weather icon and temperature in the corner of the card"
+ },
+ "weatherLocation": {
+ "label": "Weather location"
+ },
+ "isWeatherFormatFahrenheit": {
+ "label": "Use Fahrenheit"
}
}
},
diff --git a/packages/widgets/src/clock/component.tsx b/packages/widgets/src/clock/component.tsx
index 417585162..dd43e0380 100644
--- a/packages/widgets/src/clock/component.tsx
+++ b/packages/widgets/src/clock/component.tsx
@@ -1,13 +1,16 @@
"use client";
import { useEffect, useMemo, useRef, useState } from "react";
-import { Stack, Text, Title } from "@mantine/core";
+import { Box, Stack, Text, Title } from "@mantine/core";
import dayjs from "dayjs";
import advancedFormat from "dayjs/plugin/advancedFormat";
import timezones from "dayjs/plugin/timezone";
import utc from "dayjs/plugin/utc";
+import { clientApi } from "@homarr/api/client";
+
import type { WidgetComponentProps } from "../definition";
+import { AnimatedWeatherIcon } from "../weather/animated-icon";
dayjs.extend(advancedFormat);
dayjs.extend(utc);
@@ -23,30 +26,65 @@ export default function ClockWidget({ options, width }: WidgetComponentProps<"cl
const time = useCurrentTime(options);
const sizing = width < 128 ? "xs" : width < 196 ? "sm" : "md";
+ const showWeatherCorner = options.showWeather && sizing !== "xs";
return (
-
- {options.customTitleToggle && (
-
- {options.customTitle}
-
+
+ {showWeatherCorner && (
+
)}
-
- {options.customTimeFormat
- ? dayjs(time).tz(timezone).format(customTimeFormat)
- : dayjs(time).tz(timezone).format(timeFormat)}
-
- {options.showDate && (
-
- {options.customDateFormat
- ? dayjs(time).tz(timezone).format(customDateFormat)
- : dayjs(time).tz(timezone).format(dateFormat)}
-
- )}
-
+
+ {options.customTitleToggle && (
+
+ {options.customTitle}
+
+ )}
+
+ {options.customTimeFormat
+ ? dayjs(time).tz(timezone).format(customTimeFormat)
+ : dayjs(time).tz(timezone).format(timeFormat)}
+
+ {options.showDate && (
+
+ {options.customDateFormat
+ ? dayjs(time).tz(timezone).format(customDateFormat)
+ : dayjs(time).tz(timezone).format(dateFormat)}
+
+ )}
+
+
);
}
+interface ClockWeatherCornerProps {
+ latitude: number;
+ longitude: number;
+ isFahrenheit: boolean;
+}
+
+const ClockWeatherCorner = ({ latitude, longitude, isFahrenheit }: ClockWeatherCornerProps) => {
+ const { data: weather } = clientApi.widget.weather.atLocation.useQuery({ latitude, longitude });
+
+ if (!weather) return null;
+
+ const temp = isFahrenheit ? weather.current.temperature * (9 / 5) + 32 : weather.current.temperature;
+ const unit = isFahrenheit ? "°F" : "°C";
+
+ return (
+
+
+
+ {Math.round(temp)}
+ {unit}
+
+
+ );
+};
+
interface UseCurrentTimeProps {
showSeconds: boolean;
}
diff --git a/packages/widgets/src/clock/index.ts b/packages/widgets/src/clock/index.ts
index d80055a89..20b22d6a0 100644
--- a/packages/widgets/src/clock/index.ts
+++ b/packages/widgets/src/clock/index.ts
@@ -55,6 +55,20 @@ export const { definition, componentLoader } = createWidgetDefinition("clock", {
defaultValue: "",
withDescription: true,
}),
+ showWeather: factory.switch({
+ defaultValue: false,
+ withDescription: true,
+ }),
+ weatherLocation: factory.location({
+ defaultValue: {
+ name: "Brisbane",
+ latitude: -27.4698,
+ longitude: 153.0251,
+ },
+ }),
+ isWeatherFormatFahrenheit: factory.switch({
+ defaultValue: false,
+ }),
}),
{
customTitle: {
@@ -66,6 +80,12 @@ export const { definition, componentLoader } = createWidgetDefinition("clock", {
dateFormat: {
shouldHide: (options) => !options.showDate,
},
+ weatherLocation: {
+ shouldHide: (options) => !options.showWeather,
+ },
+ isWeatherFormatFahrenheit: {
+ shouldHide: (options) => !options.showWeather,
+ },
},
);
},
diff --git a/packages/widgets/src/weather/animated-icon.tsx b/packages/widgets/src/weather/animated-icon.tsx
new file mode 100644
index 000000000..70145b4b1
--- /dev/null
+++ b/packages/widgets/src/weather/animated-icon.tsx
@@ -0,0 +1,28 @@
+import "../widgets-common.css";
+
+import { WeatherIcon } from "./icon";
+
+interface AnimatedWeatherIconProps {
+ code: number;
+ size?: string | number;
+}
+
+
+const getAnimationClass = (code: number): string => {
+ if (code === 0) return "weather-anim-sun";
+ if ([1, 2, 3].includes(code)) return "weather-anim-cloud";
+ if ([45, 48].includes(code)) return "weather-anim-fog";
+ if ([51, 53, 55, 56, 57, 61, 63, 65, 66, 67, 80, 81, 82].includes(code)) return "weather-anim-rain";
+ if ([71, 73, 75, 77, 85, 86].includes(code)) return "weather-anim-snow";
+ if ([95, 96, 99].includes(code)) return "weather-anim-storm";
+ return "";
+};
+
+
+export const AnimatedWeatherIcon = ({ code, size = 26 }: AnimatedWeatherIconProps) => {
+ return (
+
+
+
+ );
+};
diff --git a/packages/widgets/src/weather/component.tsx b/packages/widgets/src/weather/component.tsx
index 27ab5d19e..0329597b1 100644
--- a/packages/widgets/src/weather/component.tsx
+++ b/packages/widgets/src/weather/component.tsx
@@ -12,7 +12,8 @@ import { useScopedI18n } from "@homarr/translation/client";
import { WidgetEmptyState } from "../common/empty-state";
import type { WidgetComponentProps } from "../definition";
-import { WeatherDescription, WeatherIcon } from "./icon";
+import { AnimatedWeatherIcon } from "./animated-icon";
+import { WeatherDescription } from "./icon";
export default function WeatherWidget({ isEditMode, options }: WidgetComponentProps<"weather">) {
const input = {
@@ -55,7 +56,7 @@ const DailyWeather = ({ options, weather }: WeatherProps) => {
-
+
@@ -146,7 +147,7 @@ const WeeklyForecast = ({ options, weather }: WeatherProps) => {
-
+
@@ -184,7 +185,7 @@ function Forecast({ weather, options }: WeatherProps) {
align="center"
>
{dayjs(dayWeather.time).format("dd")}
-
+
{getPreferredUnit(dayWeather.maxTemp, options.isFormatFahrenheit, options.disableTemperatureDecimals)}
diff --git a/packages/widgets/src/widgets-common.css b/packages/widgets/src/widgets-common.css
index 7c75ebf0a..7ea969ef1 100644
--- a/packages/widgets/src/widgets-common.css
+++ b/packages/widgets/src/widgets-common.css
@@ -25,3 +25,83 @@
.text-flash {
animation: glow 1s ease-in-out;
}
+
+/*Animated weather icons, used by clock and weather widgets*/
+.weather-anim-wrapper {
+ display: inline-flex;
+}
+
+@keyframes weather-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@keyframes weather-drift {
+ 0%,
+ 100% {
+ transform: translateX(-2px);
+ }
+ 50% {
+ transform: translateX(2px);
+ }
+}
+
+@keyframes weather-fall {
+ 0% {
+ transform: translateY(-2px);
+ opacity: 0.4;
+ }
+ 50% {
+ opacity: 1;
+ }
+ 100% {
+ transform: translateY(2px);
+ opacity: 0.4;
+ }
+}
+
+@keyframes weather-flicker {
+ 0%,
+ 90%,
+ 100% {
+ opacity: 1;
+ }
+ 92%,
+ 96% {
+ opacity: 0.4;
+ }
+}
+
+.weather-anim-sun {
+ animation: weather-spin 16s linear infinite;
+ transform-origin: center;
+}
+
+.weather-anim-cloud,
+.weather-anim-fog {
+ animation: weather-drift 4s ease-in-out infinite;
+}
+
+.weather-anim-rain,
+.weather-anim-snow {
+ animation: weather-fall 1.6s ease-in-out infinite;
+}
+
+.weather-anim-storm {
+ animation: weather-flicker 3s ease-in-out infinite;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .weather-anim-sun,
+ .weather-anim-cloud,
+ .weather-anim-fog,
+ .weather-anim-rain,
+ .weather-anim-snow,
+ .weather-anim-storm {
+ animation: none;
+ }
+}