diff --git a/apps/docs/docs/widgets/weather/index.mdx b/apps/docs/docs/widgets/weather/index.mdx
index a4b84b5fa..f36dc68ea 100644
--- a/apps/docs/docs/widgets/weather/index.mdx
+++ b/apps/docs/docs/widgets/weather/index.mdx
@@ -14,6 +14,7 @@ import { weatherWidget } from ".";
The Weather Widget uses your location to display the current, highest and lowest temperature.
+In the compact view, select a condition icon or forecast day to open details with a mouse, keyboard or touch.
The advanced view always combines current conditions with the full seven-day forecast, including humidity, wind,
gusts, sunrise and sunset. Both views reuse the same cached Open Meteo response.
diff --git a/apps/nextjs/src/components/integration/integration-grid-shared.ts b/apps/nextjs/src/components/integration/integration-grid-shared.ts
index 7451af1e7..9b4c6afb6 100644
--- a/apps/nextjs/src/components/integration/integration-grid-shared.ts
+++ b/apps/nextjs/src/components/integration/integration-grid-shared.ts
@@ -45,6 +45,7 @@ export const categoryTranslationKeys: Record = {
smartHomeServer: "integration.category.smartHomeServer",
indexerManager: "integration.category.indexerManager",
healthMonitoring: "integration.category.healthMonitoring",
+ beszel: "integration.category.beszel",
search: "integration.category.search",
mediaTranscoding: "integration.category.mediaTranscoding",
networkController: "integration.category.networkController",
diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json
index badfef807..16f33874c 100644
--- a/packages/translation/src/lang/en.json
+++ b/packages/translation/src/lang/en.json
@@ -1012,6 +1012,7 @@
"smartHomeServer": "Smart Home",
"indexerManager": "Indexer",
"healthMonitoring": "Health",
+ "beszel": "Beszel",
"search": "Search",
"mediaTranscoding": "Transcoding",
"networkController": "Network",
@@ -2299,6 +2300,7 @@
"previousMonth": "Show previous month",
"nextMonth": "Show next month",
"eventCount": "{count, plural, one {# event} other {# events}}",
+ "loading": "Loading calendar events...",
"noEvents": "No events in this month"
},
"option": {
@@ -2357,6 +2359,8 @@
}
},
"currentWindSpeed": "{currentWindSpeed} {unit}",
+ "details": "Show weather details",
+ "detailsFor": "Show weather details for {date}",
"advanced": {
"forecast": "7-day forecast",
"day": "Day",
@@ -3684,6 +3688,8 @@
"starsCount": "Stars",
"forksCount": "Forks",
"issuesCount": "Open Issues",
+ "expandDetails": "Show details for {name}",
+ "collapseDetails": "Hide details for {name}",
"markViewed": "Mark as viewed",
"openProjectPage": "Open Project Page",
"openReleasePage": "Open Release Page",
@@ -4087,6 +4093,7 @@
"executeSuccess": "Action executed successfully",
"executeFailed": "Action failed. Check the widget configuration.",
"actions": {
+ "openJsonViewer": "Open in browser JSON viewer",
"pausePolling": "Pause polling",
"resumePolling": "Resume polling",
"togglePolling": "Pause or resume polling"
diff --git a/packages/widgets/src/anchor-note/component.tsx b/packages/widgets/src/anchor-note/component.tsx
index 9268bf6f4..24e4de866 100644
--- a/packages/widgets/src/anchor-note/component.tsx
+++ b/packages/widgets/src/anchor-note/component.tsx
@@ -18,6 +18,7 @@ import { useScopedI18n } from "@homarr/translation/client";
import { WidgetEmptyState } from "../common/empty-state";
import type { WidgetComponentProps } from "../definition";
+import actionTargetClasses from "../common/action-target.module.css";
import "react-quill-new/dist/quill.snow.css";
import "./anchor-note.css";
@@ -353,7 +354,14 @@ const AnchorNoteWidgetContent = ({
) : (
-
+
diff --git a/packages/widgets/src/beszel-system-stats/component.module.css b/packages/widgets/src/beszel-system-stats/component.module.css
index 9d8b2087b..9966f37be 100644
--- a/packages/widgets/src/beszel-system-stats/component.module.css
+++ b/packages/widgets/src/beszel-system-stats/component.module.css
@@ -14,7 +14,7 @@
pointer-events: auto;
}
-@media (hover: none) {
+@media (any-pointer: coarse) {
.beszelStatsSystemToggle {
opacity: 1;
pointer-events: auto;
diff --git a/packages/widgets/src/calendar/component.tsx b/packages/widgets/src/calendar/component.tsx
index c4cee758a..614916993 100644
--- a/packages/widgets/src/calendar/component.tsx
+++ b/packages/widgets/src/calendar/component.tsx
@@ -2,7 +2,7 @@
import { useMemo, useState } from "react";
import { useParams } from "next/navigation";
-import { ActionIcon, Box, Group, Stack, Text, Tooltip, useMantineTheme } from "@mantine/core";
+import { ActionIcon, Box, Center, Group, Loader, Stack, Text, Tooltip, useMantineTheme } from "@mantine/core";
import { Calendar } from "@mantine/dates";
import { useElementSize } from "@mantine/hooks";
import { IconChevronLeft, IconChevronRight } from "@tabler/icons-react";
@@ -15,6 +15,7 @@ import type { CalendarEvent } from "@homarr/integrations/types";
import { useSettings } from "@homarr/settings";
import { useScopedI18n } from "@homarr/translation/client";
+import actionTargetClasses from "../common/action-target.module.css";
import type { WidgetComponentProps } from "../definition";
import { setWidgetRuntimeQueries } from "../definition";
import { IntegrationErrorIndicator } from "../common/integration-error-indicator";
@@ -34,7 +35,16 @@ export default function CalendarWidget(props: WidgetComponentProps<"calendar">)
if (props.integrationIds.length === 0) {
setWidgetRuntimeQueries(props.widgetStateRef, []);
- return ;
+ return (
+
+ );
}
return ;
@@ -62,7 +72,7 @@ const FetchCalendar = ({
showUnmonitored: options.showUnmonitored,
};
setWidgetRuntimeQueries(widgetStateRef, [getQueryKey(clientApi.widget.calendar.findAllEvents, input, "query")]);
- const { data } = clientApi.widget.calendar.findAllEvents.useQuery(input);
+ const { data, isPending } = clientApi.widget.calendar.findAllEvents.useQuery(input);
const events = useMemo(() => data?.flatMap((item) => item.events) ?? [], [data]);
const failedIntegrations =
@@ -75,6 +85,7 @@ const FetchCalendar = ({
isEditMode={isEditMode}
events={events}
failedIntegrations={failedIntegrations}
+ isPending={isPending}
month={month}
setMonth={setMonth}
options={options}
@@ -87,6 +98,7 @@ interface CalendarBaseProps {
isEditMode: boolean;
events: CalendarEvent[];
failedIntegrations: { integrationId: string; integrationName: string; error: string }[];
+ isPending: boolean;
month: Date;
setMonth: (date: Date) => void;
options: WidgetComponentProps<"calendar">["options"];
@@ -97,6 +109,7 @@ const CalendarBase = ({
isEditMode,
events,
failedIntegrations,
+ isPending,
month,
setMonth,
options,
@@ -133,6 +146,7 @@ const CalendarBase = ({
void;
}
-const CalendarAgenda = ({ events, failedIntegrations, isEditMode, locale, month, setMonth }: CalendarAgendaProps) => {
+const CalendarAgenda = ({
+ events,
+ failedIntegrations,
+ isPending,
+ isEditMode,
+ locale,
+ month,
+ setMonth,
+}: CalendarAgendaProps) => {
const t = useScopedI18n("widget.calendar.advanced");
const monthLabel = new Intl.DateTimeFormat(locale, { month: "long", year: "numeric" }).format(month);
@@ -243,6 +266,7 @@ const CalendarAgenda = ({ events, failedIntegrations, isEditMode, locale, month,
- {t("eventCount", { count: events.length })}
+ {isPending ? t("loading") : t("eventCount", { count: events.length })}
- {events.length > 0 ? (
+ {isPending ? (
+
+
+
+ ) : events.length > 0 ? (
) : (
diff --git a/packages/widgets/src/common/action-target.module.css b/packages/widgets/src/common/action-target.module.css
new file mode 100644
index 000000000..28f6cc672
--- /dev/null
+++ b/packages/widgets/src/common/action-target.module.css
@@ -0,0 +1,11 @@
+.root {
+ min-width: 24px;
+ min-height: 24px;
+}
+
+@media (any-pointer: coarse) {
+ .root {
+ min-width: 44px;
+ min-height: 44px;
+ }
+}
diff --git a/packages/widgets/src/coolify/resource-row.tsx b/packages/widgets/src/coolify/resource-row.tsx
index 4aaa4b625..fba5279ca 100644
--- a/packages/widgets/src/coolify/resource-row.tsx
+++ b/packages/widgets/src/coolify/resource-row.tsx
@@ -3,6 +3,7 @@
import { ActionIcon, Anchor, Group, Indicator, Stack, Text } from "@mantine/core";
import { IconFileText, IconLink } from "@tabler/icons-react";
+import actionTargetClasses from "../common/action-target.module.css";
import { cleanFqdn, getResourceTimestamp, getStatusColor, parseStatus } from "./coolify-utils";
interface ResourceRowProps {
@@ -58,6 +59,7 @@ export function ResourceRow({ item, baseUrl, isTiny, resourceType }: ResourceRow
{cleanFqdn(item.fqdn) && (
import("./custom-jsx-display"), { ssr: false });
@@ -343,14 +344,21 @@ function openJsonInBrowser(json: unknown) {
}
function RawDisplay({ data, displayMode, height }: CustomDisplayProps) {
+ const t = useScopedI18n("widget.customApi");
const maxHeight = displayMode === "advanced" ? Math.max(160, height - 80) : ((data.maxHeight as number) ?? 300);
const jsonString = JSON.stringify(data.data, null, 2);
return (
-
- openJsonInBrowser(data.data)}>
+
+ openJsonInBrowser(data.data)}
+ >
@@ -595,6 +603,7 @@ function CustomApiWidgetInner({
{dataType !== "actionButton" && (
= ({
{!isEnabled ? (
void toggleDns(data.integration.id).catch(() => undefined)}
disabled={!controlEnabled}
@@ -364,6 +366,7 @@ const ControlsCard: React.FC = ({
) : (
void toggleDns(data.integration.id).catch(() => undefined)}
disabled={!controlEnabled}
@@ -375,6 +378,7 @@ const ControlsCard: React.FC = ({
)}
{
setSelectedIntegrationIds([data.integration.id]);
@@ -440,7 +444,7 @@ const ControlsCard: React.FC = ({
{layout === "md" && (
-
+
diff --git a/packages/widgets/src/notebook/notebook.tsx b/packages/widgets/src/notebook/notebook.tsx
index 53a334189..cede6ffe4 100644
--- a/packages/widgets/src/notebook/notebook.tsx
+++ b/packages/widgets/src/notebook/notebook.tsx
@@ -68,6 +68,7 @@ import { useI18n, useScopedI18n } from "@homarr/translation/client";
import type { TablerIcon } from "@homarr/ui";
import type { WidgetComponentProps } from "../definition";
+import actionTargetClasses from "../common/action-target.module.css";
import { createReadOnlyTaskItemTransaction, ReadOnlyTaskItem } from "./read-only-task-item";
import { NotebookTextDirection, setTextDirection } from "./text-direction";
@@ -526,18 +527,12 @@ export function Notebook({
)}
{canChange && (
- <>
+
{isEditing && (
)}
- >
+
)}
);
@@ -746,13 +735,31 @@ const ColorControl = ({ defaultColor, getCurrent, update, icon: Icon, ariaLabel
-
+
-
+
-
+
diff --git a/packages/widgets/src/releases/component.tsx b/packages/widgets/src/releases/component.tsx
index 2c7470875..3481f6172 100644
--- a/packages/widgets/src/releases/component.tsx
+++ b/packages/widgets/src/releases/component.tsx
@@ -221,6 +221,10 @@ export default function ReleasesWidget({ options, itemId, width, displayMode }:
})}
onClick={() => toggleExpandedDisplay(repository)}
aria-expanded={isActive}
+ aria-label={t(isActive ? "collapseDetails" : "expandDetails", {
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ name: repository.name || repository.identifier,
+ })}
>
{(options.showDetails || isAdvanced) && (
-
+
)}
{isActive && (
void;
}
-const DetailsDisplay = ({ repository, toggleExpandedDisplay }: DetailsDisplayProps) => {
+const DetailsDisplay = ({ repository, isExpanded, toggleExpandedDisplay }: DetailsDisplayProps) => {
const t = useScopedI18n("widget.releases");
const formatter = useFormatter();
@@ -355,6 +364,11 @@ const DetailsDisplay = ({ repository, toggleExpandedDisplay }: DetailsDisplayPro
toggleExpandedDisplay(repository)}
+ aria-expanded={isExpanded}
+ aria-label={t(isExpanded ? "collapseDetails" : "expandDetails", {
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ name: repository.name || repository.identifier,
+ })}
>
diff --git a/packages/widgets/src/stocks/component.tsx b/packages/widgets/src/stocks/component.tsx
index 4fee07497..d83f409d0 100644
--- a/packages/widgets/src/stocks/component.tsx
+++ b/packages/widgets/src/stocks/component.tsx
@@ -2,7 +2,7 @@
import { Sparkline } from "@mantine/charts";
import { Flex, Stack, Text, Title, useMantineTheme } from "@mantine/core";
-import { IconTrendingDown, IconTrendingUp } from "@tabler/icons-react";
+import { IconMinus, IconTrendingDown, IconTrendingUp } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
import { useScopedI18n } from "@homarr/translation/client";
@@ -105,8 +105,10 @@ export default function StockPriceWidget({ options, width, height, displayMode }
{stockValuesChange > 0 ? (
- ) : (
+ ) : stockValuesChange < 0 ? (
+ ) : (
+
)}
{data.symbol}
diff --git a/packages/widgets/src/weather/component.module.css b/packages/widgets/src/weather/component.module.css
new file mode 100644
index 000000000..4b7b623be
--- /dev/null
+++ b/packages/widgets/src/weather/component.module.css
@@ -0,0 +1,31 @@
+.detailsButton,
+.forecastButton {
+ color: inherit;
+ border-radius: var(--mantine-radius-sm);
+}
+
+.detailsButton {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 24px;
+ min-height: 24px;
+}
+
+.forecastButton {
+ display: block;
+}
+
+.detailsButton:focus-visible,
+.forecastButton:focus-visible {
+ outline: 2px solid var(--mantine-primary-color-filled);
+ outline-offset: 2px;
+}
+
+@media (any-pointer: coarse) {
+ .detailsButton,
+ .forecastButton {
+ min-width: 44px;
+ min-height: 44px;
+ }
+}
diff --git a/packages/widgets/src/weather/component.tsx b/packages/widgets/src/weather/component.tsx
index 59f16347e..249c09f3c 100644
--- a/packages/widgets/src/weather/component.tsx
+++ b/packages/widgets/src/weather/component.tsx
@@ -1,7 +1,7 @@
"use client";
import { useId } from "react";
-import { Box, Group, HoverCard, Stack, Table, Text } from "@mantine/core";
+import { Group, Popover, Stack, Table, Text, UnstyledButton } from "@mantine/core";
import {
IconArrowDownRight,
IconArrowUpRight,
@@ -23,6 +23,7 @@ import { WidgetEmptyState } from "../common/empty-state";
import type { WidgetComponentProps } from "../definition";
import { AnimatedWeatherIcon } from "./animated-icon";
import { WeatherDescription } from "./icon";
+import classes from "./component.module.css";
export default function WeatherWidget({
isEditMode,
@@ -56,10 +57,11 @@ export default function WeatherWidget({
) : (
-
+
)}
);
@@ -69,6 +71,10 @@ interface WeatherProps extends Pick, "options">
weather: RouterOutputs["widget"]["weather"]["atLocation"];
}
+interface CompactWeatherProps extends WeatherProps {
+ isEditMode: boolean;
+}
+
const AdvancedWeather = ({ options, weather }: WeatherProps) => {
const forecastHeadingId = useId();
const t = useScopedI18n("widget.weather");
@@ -234,23 +240,23 @@ const AdvancedWeather = ({ options, weather }: WeatherProps) => {
);
};
-const DailyWeather = ({ options, weather }: WeatherProps) => {
+const DailyWeather = ({ options, weather, isEditMode }: CompactWeatherProps) => {
const t = useScopedI18n("widget.weather");
const tCommon = useScopedI18n("common");
return (
<>
-
-
-
+
+
+
-
-
-
+
+
+
-
-
+
+
{getPreferredUnit(
weather.current.temperature,
@@ -319,7 +325,9 @@ const DailyWeather = ({ options, weather }: WeatherProps) => {
);
};
-const WeeklyForecast = ({ options, weather, maxDays }: WeatherProps & { maxDays: number }) => {
+const WeeklyForecast = ({ options, weather, isEditMode, maxDays }: CompactWeatherProps & { maxDays: number }) => {
+ const t = useScopedI18n("widget.weather");
+
return (
<>
@@ -332,16 +340,16 @@ const WeeklyForecast = ({ options, weather, maxDays }: WeatherProps & { maxDays:
)}
-
-
-
+
+
+
-
-
-
+
+
+
-
-
+
+
{getPreferredUnit(
weather.current.temperature,
@@ -351,35 +359,45 @@ const WeeklyForecast = ({ options, weather, maxDays }: WeatherProps & { maxDays:
-
+
>
);
};
-function Forecast({ weather, options, maxDays }: WeatherProps & { maxDays: number }) {
+function Forecast({ weather, options, isEditMode, maxDays }: CompactWeatherProps & { maxDays: number }) {
const dateFormat = options.dateFormat;
+ const t = useScopedI18n("widget.weather");
return (
{weather.daily.slice(0, maxDays).map((dayWeather, index) => (
-
-
-
+
+
- {dayjs(dayWeather.time).format("dd")}
-
-
- {getPreferredUnit(dayWeather.maxTemp, options.isFormatFahrenheit, options.disableTemperatureDecimals)}
-
-
-
-
+
+
+ {dayjs(dayWeather.time).format("dd")}
+
+
+
+ {getPreferredUnit(dayWeather.maxTemp, options.isFormatFahrenheit, options.disableTemperatureDecimals)}
+
+
+
+
+
-
-
+
+
))}
);