fix: address widget review findings
This commit is contained in:
1 parent
842e86012d
commit
e7e44375ef
21 files changed
+190
-86
No files matched your search
@@ -22,6 +22,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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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: NonNullable<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