fix: address widget review findings
This commit is contained in:
26 files changed
+247
-94
No files matched your search
@@ -14,6 +14,7 @@ import { weatherWidget } from ".";
|
||||
<WidgetHeader widget={weatherWidget} categories={["Weather"]} />
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ export const categoryTranslationKeys: Record<string, string> = {
|
||||
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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 = ({
|
||||
</Button>
|
||||
) : (
|
||||
<Tooltip label={t("edit")}>
|
||||
<ActionIcon aria-label={t("edit")} size="md" variant="light" onClick={handleEdit} disabled={isUpdating}>
|
||||
<ActionIcon
|
||||
className={actionTargetClasses.root}
|
||||
aria-label={t("edit")}
|
||||
size="md"
|
||||
variant="light"
|
||||
onClick={handleEdit}
|
||||
disabled={isUpdating}
|
||||
>
|
||||
<IconEdit size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
@media (hover: none) {
|
||||
@media (any-pointer: coarse) {
|
||||
.beszelStatsSystemToggle {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
|
||||
@@ -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 <CalendarBase {...props} events={[]} failedIntegrations={[]} month={month} setMonth={setMonth} />;
|
||||
return (
|
||||
<CalendarBase
|
||||
{...props}
|
||||
events={[]}
|
||||
failedIntegrations={[]}
|
||||
isPending={false}
|
||||
month={month}
|
||||
setMonth={setMonth}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <FetchCalendar month={month} setMonth={setMonth} {...props} />;
|
||||
@@ -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 = ({
|
||||
<CalendarAgenda
|
||||
events={agendaEvents}
|
||||
failedIntegrations={failedIntegrations}
|
||||
isPending={isPending}
|
||||
isEditMode={isEditMode}
|
||||
locale={locale}
|
||||
month={month}
|
||||
@@ -227,13 +241,22 @@ const CalendarBase = ({
|
||||
interface CalendarAgendaProps {
|
||||
events: CalendarEvent[];
|
||||
failedIntegrations: CalendarBaseProps["failedIntegrations"];
|
||||
isPending: boolean;
|
||||
isEditMode: boolean;
|
||||
locale: string;
|
||||
month: Date;
|
||||
setMonth: (date: Date) => 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,
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
<Tooltip label={t("previousMonth")} events={{ hover: true, focus: true, touch: false }}>
|
||||
<ActionIcon
|
||||
className={actionTargetClasses.root}
|
||||
variant="subtle"
|
||||
size="lg"
|
||||
aria-label={t("previousMonth")}
|
||||
@@ -257,11 +281,12 @@ const CalendarAgenda = ({ events, failedIntegrations, isEditMode, locale, month,
|
||||
{monthLabel}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{t("eventCount", { count: events.length })}
|
||||
{isPending ? t("loading") : t("eventCount", { count: events.length })}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Tooltip label={t("nextMonth")} events={{ hover: true, focus: true, touch: false }}>
|
||||
<ActionIcon
|
||||
className={actionTargetClasses.root}
|
||||
variant="subtle"
|
||||
size="lg"
|
||||
aria-label={t("nextMonth")}
|
||||
@@ -276,7 +301,11 @@ const CalendarAgenda = ({ events, failedIntegrations, isEditMode, locale, month,
|
||||
</Group>
|
||||
|
||||
<Box style={{ flex: 1, minHeight: 0 }}>
|
||||
{events.length > 0 ? (
|
||||
{isPending ? (
|
||||
<Center component="output" h="100%" aria-label={t("loading")}>
|
||||
<Loader size="sm" />
|
||||
</Center>
|
||||
) : events.length > 0 ? (
|
||||
<CalendarEventList events={events} groupByDate locale={locale} fillHeight />
|
||||
) : (
|
||||
<Stack h="100%" align="center" justify="center" p="md">
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
.root {
|
||||
min-width: 24px;
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
@media (any-pointer: coarse) {
|
||||
.root {
|
||||
min-width: 44px;
|
||||
min-height: 44px;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
<Group wrap="nowrap" gap={4} ml={16}>
|
||||
{cleanFqdn(item.fqdn) && (
|
||||
<ActionIcon
|
||||
className={actionTargetClasses.root}
|
||||
component="a"
|
||||
href={cleanFqdn(item.fqdn)}
|
||||
target="_blank"
|
||||
@@ -72,6 +74,7 @@ export function ResourceRow({ item, baseUrl, isTiny, resourceType }: ResourceRow
|
||||
)}
|
||||
{!isTiny && logsUrl && (
|
||||
<ActionIcon
|
||||
className={actionTargetClasses.root}
|
||||
component="a"
|
||||
href={logsUrl}
|
||||
target="_blank"
|
||||
|
||||
@@ -6,6 +6,7 @@ import { IconEye, IconEyeOff, IconServer } from "@tabler/icons-react";
|
||||
import type { CoolifyServer } from "@homarr/integrations/types";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import actionTargetClasses from "../common/action-target.module.css";
|
||||
import { getBadgeColor } from "./coolify-utils";
|
||||
|
||||
interface ServersSectionProps {
|
||||
@@ -40,6 +41,7 @@ export function ServersSection({
|
||||
</Badge>
|
||||
</Group>
|
||||
<ActionIcon
|
||||
className={actionTargetClasses.root}
|
||||
aria-label={`${showIp ? tCommon("action.hide") : tCommon("action.show")} IP`}
|
||||
size="xs"
|
||||
variant="subtle"
|
||||
|
||||
@@ -32,6 +32,7 @@ import { showErrorNotification, showSuccessNotification } from "@homarr/notifica
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import actionTargetClasses from "../common/action-target.module.css";
|
||||
|
||||
const CustomJsxDisplay = dynamic(() => 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 (
|
||||
<Stack gap={4} p="xs" h="100%">
|
||||
<Group justify="flex-end">
|
||||
<Tooltip label="Open in browser JSON viewer">
|
||||
<ActionIcon variant="subtle" size="sm" onClick={() => openJsonInBrowser(data.data)}>
|
||||
<Tooltip label={t("actions.openJsonViewer")}>
|
||||
<ActionIcon
|
||||
className={actionTargetClasses.root}
|
||||
variant="subtle"
|
||||
size="sm"
|
||||
aria-label={t("actions.openJsonViewer")}
|
||||
onClick={() => openJsonInBrowser(data.data)}
|
||||
>
|
||||
<IconExternalLink size={14} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
@@ -595,6 +603,7 @@ function CustomApiWidgetInner({
|
||||
{dataType !== "actionButton" && (
|
||||
<Tooltip label={pollingLabel}>
|
||||
<ActionIcon
|
||||
className={actionTargetClasses.root}
|
||||
aria-label={pollingLabel}
|
||||
variant="subtle"
|
||||
loading={isFetching}
|
||||
|
||||
@@ -33,6 +33,7 @@ import { MaskedOrNormalImage } from "@homarr/ui";
|
||||
|
||||
import type { widgetKind } from ".";
|
||||
import type { WidgetComponentProps } from "../../definition";
|
||||
import actionTargetClasses from "../../common/action-target.module.css";
|
||||
import classes from "./component.module.css";
|
||||
import TimerModal from "./TimerModal";
|
||||
|
||||
@@ -353,6 +354,7 @@ const ControlsCard: React.FC<ControlsCardProps> = ({
|
||||
<Group gap="xs" grow wrap="nowrap" w="100%">
|
||||
{!isEnabled ? (
|
||||
<ActionIcon
|
||||
className={actionTargetClasses.root}
|
||||
aria-label={`${t("widget.dnsHoleControls.controls.enableAll")}: ${data.integration.name}`}
|
||||
onClick={() => void toggleDns(data.integration.id).catch(() => undefined)}
|
||||
disabled={!controlEnabled}
|
||||
@@ -364,6 +366,7 @@ const ControlsCard: React.FC<ControlsCardProps> = ({
|
||||
</ActionIcon>
|
||||
) : (
|
||||
<ActionIcon
|
||||
className={actionTargetClasses.root}
|
||||
aria-label={`${t("widget.dnsHoleControls.controls.disableAll")}: ${data.integration.name}`}
|
||||
onClick={() => void toggleDns(data.integration.id).catch(() => undefined)}
|
||||
disabled={!controlEnabled}
|
||||
@@ -375,6 +378,7 @@ const ControlsCard: React.FC<ControlsCardProps> = ({
|
||||
</ActionIcon>
|
||||
)}
|
||||
<ActionIcon
|
||||
className={actionTargetClasses.root}
|
||||
aria-label={`${t("widget.dnsHoleControls.controls.setTimer")}: ${data.integration.name}`}
|
||||
onClick={() => {
|
||||
setSelectedIntegrationIds([data.integration.id]);
|
||||
@@ -440,7 +444,7 @@ const ControlsCard: React.FC<ControlsCardProps> = ({
|
||||
{layout === "md" && (
|
||||
<ActionIcon
|
||||
aria-label={`${t("widget.dnsHoleControls.controls.setTimer")}: ${data.integration.name}`}
|
||||
className="dns-hole-controls-item-timer-button"
|
||||
className={combineClasses("dns-hole-controls-item-timer-button", actionTargetClasses.root)}
|
||||
display={isInteractPermitted ? undefined : "none"}
|
||||
disabled={!controlEnabled || !isEnabled}
|
||||
color="yellow"
|
||||
|
||||
@@ -43,6 +43,7 @@ import { showErrorNotification, showSuccessNotification } from "@homarr/notifica
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import actionTargetClasses from "../common/action-target.module.css";
|
||||
import { HomarrDataTable } from "../common/homarr-data-table";
|
||||
import { usePersistedTableLayout, useTableLayoutPersistence } from "../common/use-persisted-table-layout";
|
||||
import { getDockerColumnVisibility, getDockerFooterVisibility } from "./layout";
|
||||
@@ -409,6 +410,7 @@ export default function DockerWidget({
|
||||
)}
|
||||
<Tooltip label={t("table.refresh.lastUpdated", { when: relativeTime })}>
|
||||
<ActionIcon
|
||||
className={actionTargetClasses.root}
|
||||
size="sm"
|
||||
variant="transparent"
|
||||
c="var(--mantine-color-text)"
|
||||
@@ -443,6 +445,7 @@ function ContainerMenuButton({
|
||||
<Menu opened={opened} onChange={setOpened} closeOnItemClick={false} withinPortal position="bottom-end" shadow="sm">
|
||||
<Menu.Target>
|
||||
<ActionIcon
|
||||
className={actionTargetClasses.root}
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
size="sm"
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
@media (hover: none) {
|
||||
@media (any-pointer: coarse) {
|
||||
.infoActionCompact {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
|
||||
@@ -42,6 +42,7 @@ import { useI18n } from "@homarr/translation/client";
|
||||
import { filterStorageVolumes, normalizeStorageDeviceName } from "../filter-storage-volumes";
|
||||
import { WidgetEmptyState } from "../common/empty-state";
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import actionTargetClasses from "../common/action-target.module.css";
|
||||
import { CpuRing } from "./rings/cpu-ring";
|
||||
import { CpuTempRing } from "./rings/cpu-temp-ring";
|
||||
import { GpuRing } from "./rings/gpu-ring";
|
||||
@@ -118,7 +119,7 @@ export const SystemHealthMonitoring = ({
|
||||
disabled={!healthInfo.rebootRequired && healthInfo.availablePkgUpdates === 0}
|
||||
>
|
||||
<ActionIcon
|
||||
className="health-monitoring-information-icon-avatar"
|
||||
className={combineClasses("health-monitoring-information-icon-avatar", actionTargetClasses.root)}
|
||||
variant={"light"}
|
||||
color="var(--mantine-color-text)"
|
||||
size="sm"
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (hover: none) {
|
||||
@media (any-pointer: coarse) {
|
||||
.testAction {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useRequiredBoard } from "@homarr/boards/context";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import actionTargetClasses from "../common/action-target.module.css";
|
||||
import classes from "./component.module.css";
|
||||
|
||||
export default function IndexerManagerWidget({
|
||||
@@ -83,7 +84,7 @@ export default function IndexerManagerWidget({
|
||||
)}
|
||||
<Tooltip label={t("widget.indexerManager.testAll")}>
|
||||
<ActionIcon
|
||||
className={combineClasses("indexer-manager-test-action-icon", classes.testAction)}
|
||||
className={combineClasses("indexer-manager-test-action-icon", classes.testAction, actionTargetClasses.root)}
|
||||
size="sm"
|
||||
radius={board.itemRadius}
|
||||
variant="light"
|
||||
|
||||
@@ -24,19 +24,12 @@
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@media (hover: none) {
|
||||
@media (any-pointer: coarse) {
|
||||
.pendingActions {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (pointer: coarse) {
|
||||
.pendingActions button {
|
||||
min-width: 36px;
|
||||
min-height: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.card,
|
||||
.pendingActions {
|
||||
|
||||
@@ -14,6 +14,7 @@ import { openMediaRequestSearch } from "@homarr/spotlight";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import { WidgetEmptyState } from "../../common/empty-state";
|
||||
import actionTargetClasses from "../../common/action-target.module.css";
|
||||
import { IntegrationErrorIndicator } from "../../common/integration-error-indicator";
|
||||
import type { WidgetComponentProps } from "../../definition";
|
||||
import { NoIntegrationDataError } from "../../errors/no-data-integration";
|
||||
@@ -83,7 +84,7 @@ const MediaRequestSearchButton = ({ integrationIds }: { integrationIds: string[]
|
||||
return (
|
||||
<Tooltip label={t("action.search.label")}>
|
||||
<ActionIcon
|
||||
className={searchClasses.searchButton}
|
||||
className={`${searchClasses.searchButton} ${actionTargetClasses.root}`}
|
||||
variant="light"
|
||||
size="sm"
|
||||
aria-label={t("action.search.label")}
|
||||
@@ -255,7 +256,7 @@ const DecisionButtons = ({ requestId, integrationId, canInteract, alwaysVisible
|
||||
>
|
||||
<Tooltip label={t("pending.approve")}>
|
||||
<ActionIcon
|
||||
className="mediaRequests-list-item-pending-button-approve"
|
||||
className={`mediaRequests-list-item-pending-button-approve ${actionTargetClasses.root}`}
|
||||
variant="light"
|
||||
color="green"
|
||||
size="sm"
|
||||
@@ -270,7 +271,7 @@ const DecisionButtons = ({ requestId, integrationId, canInteract, alwaysVisible
|
||||
</Tooltip>
|
||||
<Tooltip label={t("pending.decline")}>
|
||||
<ActionIcon
|
||||
className="mediaRequests-list-item-pending-button-decline"
|
||||
className={`mediaRequests-list-item-pending-button-decline ${actionTargetClasses.root}`}
|
||||
variant="light"
|
||||
color="red"
|
||||
size="sm"
|
||||
|
||||
@@ -17,7 +17,11 @@
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@media (hover: none) {
|
||||
@media (any-pointer: coarse) {
|
||||
.searchRoot {
|
||||
padding-top: 52px;
|
||||
}
|
||||
|
||||
.searchButton {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import { openMediaRequestSearch } from "@homarr/spotlight";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import { WidgetEmptyState } from "../../common/empty-state";
|
||||
import actionTargetClasses from "../../common/action-target.module.css";
|
||||
import { IntegrationErrorIndicator } from "../../common/integration-error-indicator";
|
||||
import type { WidgetComponentProps } from "../../definition";
|
||||
import { NoIntegrationDataError } from "../../errors/no-data-integration";
|
||||
@@ -220,7 +221,7 @@ const MediaRequestSearchButton = ({ integrationIds }: { integrationIds: string[]
|
||||
return (
|
||||
<Tooltip label={t("action.search.label")}>
|
||||
<ActionIcon
|
||||
className={searchClasses.searchButton}
|
||||
className={`${searchClasses.searchButton} ${actionTargetClasses.root}`}
|
||||
variant="light"
|
||||
size="sm"
|
||||
aria-label={t("action.search.label")}
|
||||
|
||||
@@ -6,6 +6,8 @@ import { IconHeartbeat } from "@tabler/icons-react";
|
||||
import type { TdarrStatistics } from "@homarr/integrations";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import actionTargetClasses from "../common/action-target.module.css";
|
||||
|
||||
interface HealthCheckStatusProps {
|
||||
statistics: TdarrStatistics;
|
||||
}
|
||||
@@ -23,7 +25,7 @@ export function HealthCheckStatus(props: HealthCheckStatusProps) {
|
||||
return (
|
||||
<Popover position="bottom" width={250} shadow="sm" withArrow>
|
||||
<Popover.Target>
|
||||
<ActionIcon variant="subtle" size="sm" aria-label={t("title")}>
|
||||
<ActionIcon className={actionTargetClasses.root} variant="subtle" size="sm" aria-label={t("title")}>
|
||||
<Indicator color={textColor(indicatorColor, colorScheme)} size={6} display="flex">
|
||||
<IconHeartbeat size={16} />
|
||||
</Indicator>
|
||||
|
||||
@@ -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({
|
||||
</Group>
|
||||
)}
|
||||
{canChange && (
|
||||
<>
|
||||
<Stack pos="absolute" top={7} right={7} gap={7} style={{ zIndex: 1 }}>
|
||||
<ActionIcon
|
||||
className="homarr-notebook-action"
|
||||
className={`homarr-notebook-action ${actionTargetClasses.root}`}
|
||||
data-visible={isEditing || displayMode === "advanced" || undefined}
|
||||
title={isEditing ? t("common.action.save") : t("common.action.edit")}
|
||||
aria-label={isEditing ? t("common.action.save") : t("common.action.edit")}
|
||||
style={{
|
||||
zIndex: 1,
|
||||
}}
|
||||
top={7}
|
||||
right={7}
|
||||
pos="absolute"
|
||||
color={primaryColor}
|
||||
variant="light"
|
||||
size={30}
|
||||
@@ -549,16 +544,10 @@ export function Notebook({
|
||||
</ActionIcon>
|
||||
{isEditing && (
|
||||
<ActionIcon
|
||||
className="homarr-notebook-action"
|
||||
className={`homarr-notebook-action ${actionTargetClasses.root}`}
|
||||
data-visible
|
||||
title={t("common.action.cancel")}
|
||||
aria-label={t("common.action.cancel")}
|
||||
style={{
|
||||
zIndex: 1,
|
||||
}}
|
||||
top={44}
|
||||
right={7}
|
||||
pos="absolute"
|
||||
color={primaryColor}
|
||||
variant="light"
|
||||
size={30}
|
||||
@@ -568,7 +557,7 @@ export function Notebook({
|
||||
<IconX {...iconProps} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
</>
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
@@ -746,13 +735,31 @@ const ColorControl = ({ defaultColor, getCurrent, update, icon: Icon, ariaLabel
|
||||
<Stack gap={8}>
|
||||
<ColorPicker value={color} onChange={setColor} format="hexa" swatches={palette} swatchesPerRow={6} />
|
||||
<Group justify="right" gap={8}>
|
||||
<ActionIcon title={t("common.action.cancel")} variant="default" onClick={close}>
|
||||
<ActionIcon
|
||||
className={actionTargetClasses.root}
|
||||
title={t("common.action.cancel")}
|
||||
aria-label={t("common.action.cancel")}
|
||||
variant="default"
|
||||
onClick={close}
|
||||
>
|
||||
<IconX stroke={1.5} size="1rem" />
|
||||
</ActionIcon>
|
||||
<ActionIcon title={t("common.action.apply")} variant="default" onClick={handleApplyColor}>
|
||||
<ActionIcon
|
||||
className={actionTargetClasses.root}
|
||||
title={t("common.action.apply")}
|
||||
aria-label={t("common.action.apply")}
|
||||
variant="default"
|
||||
onClick={handleApplyColor}
|
||||
>
|
||||
<IconCheck stroke={1.5} size="1rem" />
|
||||
</ActionIcon>
|
||||
<ActionIcon title={t("widget.notebook.popover.clearColor")} variant="default" onClick={handleClearColor}>
|
||||
<ActionIcon
|
||||
className={actionTargetClasses.root}
|
||||
title={t("widget.notebook.popover.clearColor")}
|
||||
aria-label={t("widget.notebook.popover.clearColor")}
|
||||
variant="default"
|
||||
onClick={handleClearColor}
|
||||
>
|
||||
<IconCircleOff stroke={1.5} size="1rem" />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
|
||||
@@ -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,
|
||||
})}
|
||||
>
|
||||
<Group p="xs">
|
||||
<MaskedOrNormalImage
|
||||
@@ -321,7 +325,11 @@ export default function ReleasesWidget({ options, itemId, width, displayMode }:
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
{(options.showDetails || isAdvanced) && (
|
||||
<DetailsDisplay repository={repository} toggleExpandedDisplay={toggleExpandedDisplay} />
|
||||
<DetailsDisplay
|
||||
repository={repository}
|
||||
isExpanded={isActive}
|
||||
toggleExpandedDisplay={toggleExpandedDisplay}
|
||||
/>
|
||||
)}
|
||||
{isActive && (
|
||||
<ExpandedDisplay
|
||||
@@ -342,10 +350,11 @@ export default function ReleasesWidget({ options, itemId, width, displayMode }:
|
||||
|
||||
interface DetailsDisplayProps {
|
||||
repository: ReleasesRepositoryResponse;
|
||||
isExpanded: boolean;
|
||||
toggleExpandedDisplay: (repository: ReleasesRepositoryResponse) => 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
|
||||
<UnstyledButton
|
||||
className={combineClasses("releases-repository-details", classes.releasesRepositoryDetails)}
|
||||
onClick={() => 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,
|
||||
})}
|
||||
>
|
||||
<Group justify="space-between" p={5}>
|
||||
<Group className="releases-repository-details-icon-wrapper">
|
||||
|
||||
@@ -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 }
|
||||
<Text size="xl" fw={700} lh="0.715">
|
||||
{stockValuesChange > 0 ? (
|
||||
<IconTrendingUp size="1.5rem" color={theme.colors.green[7]} />
|
||||
) : (
|
||||
) : stockValuesChange < 0 ? (
|
||||
<IconTrendingDown size="1.5rem" color={theme.colors.red[7]} />
|
||||
) : (
|
||||
<IconMinus size="1.5rem" color={theme.colors.gray[6]} />
|
||||
)}
|
||||
{data.symbol}
|
||||
</Text>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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({
|
||||
<WeeklyForecast
|
||||
weather={weather}
|
||||
options={options}
|
||||
isEditMode={isEditMode}
|
||||
maxDays={getVisibleForecastDays(options.forecastDayCount, width, height)}
|
||||
/>
|
||||
) : (
|
||||
<DailyWeather weather={weather} options={options} />
|
||||
<DailyWeather weather={weather} options={options} isEditMode={isEditMode} />
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
@@ -69,6 +71,10 @@ interface WeatherProps extends Pick<WidgetComponentProps<"weather">, "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 (
|
||||
<>
|
||||
<Group className="weather-day-group" gap="sm">
|
||||
<HoverCard>
|
||||
<HoverCard.Target>
|
||||
<Box>
|
||||
<Popover position="bottom" withArrow shadow="md">
|
||||
<Popover.Target>
|
||||
<UnstyledButton className={classes.detailsButton} aria-label={t("details")} disabled={isEditMode}>
|
||||
<AnimatedWeatherIcon size={30} code={weather.current.weathercode} />
|
||||
</Box>
|
||||
</HoverCard.Target>
|
||||
<HoverCard.Dropdown>
|
||||
</UnstyledButton>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown>
|
||||
<WeatherDescription weatherOnly weatherCode={weather.current.weathercode} />
|
||||
</HoverCard.Dropdown>
|
||||
</HoverCard>
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
<Text fz={30}>
|
||||
{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 (
|
||||
<>
|
||||
<Group className="weather-forecast-city-temp-group" wrap="nowrap" gap="md">
|
||||
@@ -332,16 +340,16 @@ const WeeklyForecast = ({ options, weather, maxDays }: WeatherProps & { maxDays:
|
||||
</Group>
|
||||
)}
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
<HoverCard>
|
||||
<HoverCard.Target>
|
||||
<Box>
|
||||
<Popover position="bottom" withArrow shadow="md">
|
||||
<Popover.Target>
|
||||
<UnstyledButton className={classes.detailsButton} aria-label={t("details")} disabled={isEditMode}>
|
||||
<AnimatedWeatherIcon size={16} code={weather.current.weathercode} />
|
||||
</Box>
|
||||
</HoverCard.Target>
|
||||
<HoverCard.Dropdown>
|
||||
</UnstyledButton>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown>
|
||||
<WeatherDescription weatherOnly weatherCode={weather.current.weathercode} />
|
||||
</HoverCard.Dropdown>
|
||||
</HoverCard>
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
<Text fz={16}>
|
||||
{getPreferredUnit(
|
||||
weather.current.temperature,
|
||||
@@ -351,35 +359,45 @@ const WeeklyForecast = ({ options, weather, maxDays }: WeatherProps & { maxDays:
|
||||
</Text>
|
||||
</Group>
|
||||
</Group>
|
||||
<Forecast weather={weather} options={options} maxDays={maxDays} />
|
||||
<Forecast weather={weather} options={options} isEditMode={isEditMode} maxDays={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 (
|
||||
<Group className="weather-forecast-days-group" w="100%" justify="space-evenly" wrap="nowrap" pb="sm">
|
||||
{weather.daily.slice(0, maxDays).map((dayWeather, index) => (
|
||||
<HoverCard key={dayWeather.time} withArrow shadow="md">
|
||||
<HoverCard.Target>
|
||||
<Stack
|
||||
className={combineClasses(
|
||||
"weather-forecast-day-stack",
|
||||
`weather-forecast-day${index}`,
|
||||
`weather-forecast-weekday${dayjs(dayWeather.time).day()}`,
|
||||
)}
|
||||
gap="0"
|
||||
align="center"
|
||||
<Popover key={dayWeather.time} position="bottom" withArrow shadow="md">
|
||||
<Popover.Target>
|
||||
<UnstyledButton
|
||||
className={classes.forecastButton}
|
||||
aria-label={t("detailsFor", { date: dayjs(dayWeather.time).format(dateFormat) })}
|
||||
disabled={isEditMode}
|
||||
>
|
||||
<Text fz="xl">{dayjs(dayWeather.time).format("dd")}</Text>
|
||||
<AnimatedWeatherIcon size={16} code={dayWeather.weatherCode} />
|
||||
<Text fz={16}>
|
||||
{getPreferredUnit(dayWeather.maxTemp, options.isFormatFahrenheit, options.disableTemperatureDecimals)}
|
||||
</Text>
|
||||
</Stack>
|
||||
</HoverCard.Target>
|
||||
<HoverCard.Dropdown>
|
||||
<Stack
|
||||
component="span"
|
||||
className={combineClasses(
|
||||
"weather-forecast-day-stack",
|
||||
`weather-forecast-day${index}`,
|
||||
`weather-forecast-weekday${dayjs(dayWeather.time).day()}`,
|
||||
)}
|
||||
gap="0"
|
||||
align="center"
|
||||
>
|
||||
<Text component="span" fz="xl">
|
||||
{dayjs(dayWeather.time).format("dd")}
|
||||
</Text>
|
||||
<AnimatedWeatherIcon size={16} code={dayWeather.weatherCode} />
|
||||
<Text component="span" fz={16}>
|
||||
{getPreferredUnit(dayWeather.maxTemp, options.isFormatFahrenheit, options.disableTemperatureDecimals)}
|
||||
</Text>
|
||||
</Stack>
|
||||
</UnstyledButton>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown>
|
||||
<WeatherDescription
|
||||
useImperialSpeed={options.useImperialSpeed}
|
||||
dateFormat={dateFormat}
|
||||
@@ -401,8 +419,8 @@ function Forecast({ weather, options, maxDays }: WeatherProps & { maxDays: numbe
|
||||
maxWindGusts={dayWeather.maxWindGusts}
|
||||
humidity={dayWeather.humidity}
|
||||
/>
|
||||
</HoverCard.Dropdown>
|
||||
</HoverCard>
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
))}
|
||||
</Group>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user