chore(release): automatic release v1.10.0

This commit is contained in:
homarr-releases[bot]
2025-03-07 19:13:16 +00:00
committed by GitHub
39 changed files with 728 additions and 708 deletions

View File

@@ -31,6 +31,9 @@ body:
label: Version label: Version
description: What version of Homarr are you running? description: What version of Homarr are you running?
options: options:
- 1.9.0
- 1.8.0
- 1.7.0
- 1.6.0 - 1.6.0
- 1.5.0 - 1.5.0
- 1.4.0 - 1.4.0

View File

@@ -56,9 +56,9 @@
"@mantine/tiptap": "^7.17.1", "@mantine/tiptap": "^7.17.1",
"@million/lint": "1.0.14", "@million/lint": "1.0.14",
"@tabler/icons-react": "^3.31.0", "@tabler/icons-react": "^3.31.0",
"@tanstack/react-query": "^5.67.1", "@tanstack/react-query": "^5.67.2",
"@tanstack/react-query-devtools": "^5.67.1", "@tanstack/react-query-devtools": "^5.67.2",
"@tanstack/react-query-next-experimental": "^5.67.1", "@tanstack/react-query-next-experimental": "^5.67.2",
"@trpc/client": "next", "@trpc/client": "next",
"@trpc/next": "next", "@trpc/next": "next",
"@trpc/react-query": "next", "@trpc/react-query": "next",

View File

@@ -40,8 +40,8 @@
"@semantic-release/release-notes-generator": "^14.0.3", "@semantic-release/release-notes-generator": "^14.0.3",
"@turbo/gen": "^2.4.4", "@turbo/gen": "^2.4.4",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.3.4",
"@vitest/coverage-v8": "^3.0.7", "@vitest/coverage-v8": "^3.0.8",
"@vitest/ui": "^3.0.7", "@vitest/ui": "^3.0.8",
"conventional-changelog-conventionalcommits": "^8.0.0", "conventional-changelog-conventionalcommits": "^8.0.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"jsdom": "^26.0.0", "jsdom": "^26.0.0",
@@ -51,7 +51,7 @@
"turbo": "^2.4.4", "turbo": "^2.4.4",
"typescript": "^5.8.2", "typescript": "^5.8.2",
"vite-tsconfig-paths": "^5.1.4", "vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.0.7" "vitest": "^3.0.8"
}, },
"packageManager": "pnpm@10.5.2", "packageManager": "pnpm@10.5.2",
"engines": { "engines": {

View File

@@ -52,7 +52,7 @@
"drizzle-kit": "^0.30.5", "drizzle-kit": "^0.30.5",
"drizzle-orm": "^0.40.0", "drizzle-orm": "^0.40.0",
"drizzle-zod": "^0.7.0", "drizzle-zod": "^0.7.0",
"mysql2": "3.12.0" "mysql2": "3.13.0"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",

View File

@@ -1,3 +1,4 @@
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
import { removeTrailingSlash } from "@homarr/common"; import { removeTrailingSlash } from "@homarr/common";
import type { IntegrationInput } from "../base/integration"; import type { IntegrationInput } from "../base/integration";
@@ -7,7 +8,7 @@ import { PiHoleIntegrationV6 } from "./v6/pi-hole-integration-v6";
export const createPiHoleIntegrationAsync = async (input: IntegrationInput) => { export const createPiHoleIntegrationAsync = async (input: IntegrationInput) => {
const baseUrl = removeTrailingSlash(input.url); const baseUrl = removeTrailingSlash(input.url);
const url = new URL(`${baseUrl}/api/info/version`); const url = new URL(`${baseUrl}/api/info/version`);
const response = await fetch(url); const response = await fetchWithTrustedCertificatesAsync(url);
/** /**
* In pi-hole 5 the api was at /admin/api.php, in pi-hole 6 it was moved to /api * In pi-hole 5 the api was at /admin/api.php, in pi-hole 6 it was moved to /api

View File

@@ -24,7 +24,7 @@
"prettier": "@homarr/prettier-config", "prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@homarr/env": "workspace:^0.1.0", "@homarr/env": "workspace:^0.1.0",
"ioredis": "5.5.0", "ioredis": "5.6.0",
"superjson": "2.2.2", "superjson": "2.2.2",
"winston": "3.17.0", "winston": "3.17.0",
"zod": "^3.24.2" "zod": "^3.24.2"

View File

@@ -26,7 +26,7 @@
"@homarr/db": "workspace:^", "@homarr/db": "workspace:^",
"@homarr/definitions": "workspace:^", "@homarr/definitions": "workspace:^",
"@homarr/log": "workspace:^", "@homarr/log": "workspace:^",
"ioredis": "5.5.0", "ioredis": "5.6.0",
"superjson": "2.2.2" "superjson": "2.2.2"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -974,7 +974,7 @@
}, },
"option": { "option": {
"borderColor": { "borderColor": {
"label": "" "label": "边界颜色"
} }
}, },
"remove": { "remove": {
@@ -1822,10 +1822,10 @@
"available": "可用" "available": "可用"
}, },
"status": { "status": {
"pending": "", "pending": "待处理",
"approved": "", "approved": "已批准",
"declined": "", "declined": "已拒绝",
"failed": "" "failed": "失败"
}, },
"toBeDetermined": "待定" "toBeDetermined": "待定"
}, },

View File

@@ -974,7 +974,7 @@
}, },
"option": { "option": {
"borderColor": { "borderColor": {
"label": "" "label": "צבע מסגרת"
} }
}, },
"remove": { "remove": {
@@ -1822,10 +1822,10 @@
"available": "זמין" "available": "זמין"
}, },
"status": { "status": {
"pending": "", "pending": "ממתין",
"approved": "", "approved": "אושר",
"declined": "", "declined": "נדחה",
"failed": "" "failed": "נכשל"
}, },
"toBeDetermined": "ייקבע בהמשך" "toBeDetermined": "ייקבע בהמשך"
}, },

View File

@@ -784,7 +784,7 @@
}, },
"tokenId": { "tokenId": {
"label": "Token Anahtar Kimliği", "label": "Token Anahtar Kimliği",
"newLabel": "Yeni Anahtar Kimliği" "newLabel": "Yeni Token Anahtar Kimliği"
}, },
"realm": { "realm": {
"label": "Erişim Alanı", "label": "Erişim Alanı",

View File

@@ -974,7 +974,7 @@
}, },
"option": { "option": {
"borderColor": { "borderColor": {
"label": "" "label": "邊框顏色"
} }
}, },
"remove": { "remove": {
@@ -1822,10 +1822,10 @@
"available": "待定" "available": "待定"
}, },
"status": { "status": {
"pending": "", "pending": "待處理",
"approved": "", "approved": "已批准",
"declined": "", "declined": "已拒絕",
"failed": "" "failed": "失敗"
}, },
"toBeDetermined": "多媒體請求狀態" "toBeDetermined": "多媒體請求狀態"
}, },

View File

@@ -74,12 +74,11 @@ export default function AppWidget({ options, isEditMode }: WidgetComponentProps<
h="100%" h="100%"
w="100%" w="100%"
direction="column" direction="column"
p="7.5cqmin"
justify="center" justify="center"
align="center" align="center"
> >
{options.showTitle && ( {options.showTitle && (
<Text className="app-title" fw={700} ta="center" size="12.5cqmin"> <Text className="app-title" fw={700} ta="center">
{app.name} {app.name}
</Text> </Text>
)} )}

View File

@@ -14,18 +14,18 @@ export const PingDot = ({ color, tooltip, ...props }: PingDotProps) => {
const { pingIconsEnabled } = useSettings(); const { pingIconsEnabled } = useSettings();
return ( return (
<Box bottom="2.5cqmin" right="2.5cqmin" pos="absolute"> <Box bottom={10} right={10} pos="absolute" display={"flex"}>
<Tooltip label={tooltip}> <Tooltip label={tooltip}>
{pingIconsEnabled ? ( {pingIconsEnabled ? (
<props.icon style={{ width: "10cqmin", height: "10cqmin" }} color={color} /> <props.icon style={{ width: 20, height: 20 }} strokeWidth={5} color={color} />
) : ( ) : (
<Box <Box
bg={color} bg={color}
style={{ style={{
borderRadius: "100%", borderRadius: "100%",
}} }}
w="10cqmin" w={16}
h="10cqmin" h={16}
></Box> ></Box>
)} )}
</Tooltip> </Tooltip>

View File

@@ -2,6 +2,14 @@
background-color: var(--mantine-color-primaryColor-light-hover); background-color: var(--mantine-color-primaryColor-light-hover);
} }
[data-mantine-color-scheme="light"] .card-grid {
background-color: var(--mantine-color-gray-1);
}
[data-mantine-color-scheme="dark"] .card-grid {
background-color: var(--mantine-color-dark-7);
}
.card:hover > div > div.bookmarkIcon { .card:hover > div > div.bookmarkIcon {
background-color: var(--mantine-color-iconColor-filled-hover); background-color: var(--mantine-color-iconColor-filled-hover);
} }

View File

@@ -1,6 +1,7 @@
"use client"; "use client";
import { Anchor, Box, Card, Divider, Flex, Group, Stack, Text, Title, UnstyledButton } from "@mantine/core"; import { Anchor, Box, Card, Divider, Flex, Group, Stack, Text, Title, UnstyledButton } from "@mantine/core";
import combineClasses from "clsx";
import type { RouterOutputs } from "@homarr/api"; import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
@@ -42,9 +43,11 @@ export default function BookmarksWidget({ options, width, height, itemId }: Widg
return ( return (
<Stack h="100%" gap="sm" p="sm"> <Stack h="100%" gap="sm" p="sm">
<Title order={4} px="0.25rem"> {options.title.length > 0 && (
{options.title} <Title order={4} px="0.25rem">
</Title> {options.title}
</Title>
)}
{options.layout === "grid" && ( {options.layout === "grid" && (
<GridLayout <GridLayout
data={data} data={data}
@@ -80,8 +83,9 @@ interface FlexLayoutProps {
} }
const FlexLayout = ({ data, direction, hideIcon, hideHostname, openNewTab, hasIconColor }: FlexLayoutProps) => { const FlexLayout = ({ data, direction, hideIcon, hideHostname, openNewTab, hasIconColor }: FlexLayoutProps) => {
const board = useRequiredBoard();
return ( return (
<Flex direction={direction} gap="0" h="100%" w="100%"> <Flex direction={direction} gap="0" w="100%">
{data.map((app, index) => ( {data.map((app, index) => (
<div key={app.id} style={{ display: "flex", flex: "1", flexDirection: direction }}> <div key={app.id} style={{ display: "flex", flex: "1", flexDirection: direction }}>
<Divider <Divider
@@ -95,18 +99,9 @@ const FlexLayout = ({ data, direction, hideIcon, hideHostname, openNewTab, hasIc
target={openNewTab ? "_blank" : "_self"} target={openNewTab ? "_blank" : "_self"}
rel="noopener noreferrer" rel="noopener noreferrer"
key={app.id} key={app.id}
h="100%"
w="100%" w="100%"
> >
<Card <Card radius={board.itemRadius} className={classes.card} w="100%" display="flex" p={"xs"} h={"100%"}>
radius="md"
style={{ containerType: "size" }}
className={classes.card}
h="100%"
w="100%"
display="flex"
p={0}
>
{direction === "row" ? ( {direction === "row" ? (
<VerticalItem app={app} hideIcon={hideIcon} hideHostname={hideHostname} hasIconColor={hasIconColor} /> <VerticalItem app={app} hideIcon={hideIcon} hideHostname={hideHostname} hasIconColor={hasIconColor} />
) : ( ) : (
@@ -134,12 +129,14 @@ const GridLayout = ({ data, width, height, hideIcon, hideHostname, openNewTab, h
// Calculates the perfect number of columns for the grid layout based on the width and height in pixels and the number of items // Calculates the perfect number of columns for the grid layout based on the width and height in pixels and the number of items
const columns = Math.ceil(Math.sqrt(data.length * (width / height))); const columns = Math.ceil(Math.sqrt(data.length * (width / height)));
const board = useRequiredBoard();
return ( return (
<Box <Box
display="grid" display="grid"
h="100%" h="100%"
style={{ style={{
gridTemplateColumns: `repeat(${columns}, auto)`, gridTemplateColumns: `repeat(${columns}, 1fr)`,
gap: 10, gap: 10,
}} }}
> >
@@ -152,8 +149,19 @@ const GridLayout = ({ data, width, height, hideIcon, hideHostname, openNewTab, h
key={app.id} key={app.id}
h="100%" h="100%"
> >
<Card withBorder style={{ containerType: "size" }} h="100%" className={classes.card} p="5cqmin"> <Card
<VerticalItem app={app} hideIcon={hideIcon} hideHostname={hideHostname} hasIconColor={hasIconColor} /> h="100%"
className={combineClasses(classes.card, classes["card-grid"])}
radius={board.itemRadius}
p="sm"
>
<VerticalItem
app={app}
hideIcon={hideIcon}
hideHostname={hideHostname}
hasIconColor={hasIconColor}
size={50}
/>
</Card> </Card>
</UnstyledButton> </UnstyledButton>
))} ))}
@@ -166,15 +174,17 @@ const VerticalItem = ({
hideIcon, hideIcon,
hideHostname, hideHostname,
hasIconColor, hasIconColor,
size = 30,
}: { }: {
app: RouterOutputs["app"]["byIds"][number]; app: RouterOutputs["app"]["byIds"][number];
hideIcon: boolean; hideIcon: boolean;
hideHostname: boolean; hideHostname: boolean;
hasIconColor: boolean; hasIconColor: boolean;
size?: number;
}) => { }) => {
return ( return (
<Stack h="100%" gap="5cqmin"> <Stack h="100%" gap="sm">
<Text fw={700} ta="center" size="20cqmin"> <Text fw={700} ta="center" size="lg">
{app.name} {app.name}
</Text> </Text>
{!hideIcon && ( {!hideIcon && (
@@ -184,16 +194,18 @@ const VerticalItem = ({
alt={app.name} alt={app.name}
className={classes.bookmarkIcon} className={classes.bookmarkIcon}
style={{ style={{
maxHeight: "100%", width: size,
maxWidth: "100%", height: size,
overflow: "auto", overflow: "auto",
flex: 1, flex: 1,
scale: 0.8, scale: 0.8,
marginLeft: "auto",
marginRight: "auto",
}} }}
/> />
)} )}
{!hideHostname && ( {!hideHostname && (
<Anchor ta="center" component="span" size="12cqmin"> <Anchor ta="center" component="span" size="lg">
{app.href ? new URL(app.href).hostname : undefined} {app.href ? new URL(app.href).hostname : undefined}
</Anchor> </Anchor>
)} )}
@@ -213,7 +225,7 @@ const HorizontalItem = ({
hasIconColor: boolean; hasIconColor: boolean;
}) => { }) => {
return ( return (
<Group wrap="nowrap"> <Group wrap="nowrap" gap={"xs"}>
{!hideIcon && ( {!hideIcon && (
<MaskedOrNormalImage <MaskedOrNormalImage
imageUrl={app.iconUrl} imageUrl={app.iconUrl}
@@ -223,20 +235,19 @@ const HorizontalItem = ({
style={{ style={{
overflow: "auto", overflow: "auto",
scale: 0.8, scale: 0.8,
minHeight: "100cqh", width: 30,
maxHeight: "100cqh", height: 30,
minWidth: "100cqh", flex: "unset",
maxWidth: "100cqh",
}} }}
/> />
)} )}
<Stack justify="space-between" gap={0}> <Stack justify="space-between" gap={0}>
<Text fw={700} size="45cqh" lineClamp={1}> <Text fw={700} size="md" lineClamp={1}>
{app.name} {app.name}
</Text> </Text>
{!hideHostname && ( {!hideHostname && (
<Anchor component="span" size="30cqh"> <Anchor component="span" size="xs">
{app.href ? new URL(app.href).hostname : undefined} {app.href ? new URL(app.href).hostname : undefined}
</Anchor> </Anchor>
)} )}

View File

@@ -1,6 +1,7 @@
import { useState } from "react"; import { useState } from "react";
import { Container, Popover, useMantineTheme } from "@mantine/core"; import { Box, Container, Flex, Popover, Text, useMantineTheme } from "@mantine/core";
import { useRequiredBoard } from "@homarr/boards/context";
import type { CalendarEvent } from "@homarr/integrations/types"; import type { CalendarEvent } from "@homarr/integrations/types";
import { CalendarEventList } from "./calendar-event-list"; import { CalendarEventList } from "./calendar-event-list";
@@ -9,11 +10,19 @@ interface CalendarDayProps {
date: Date; date: Date;
events: CalendarEvent[]; events: CalendarEvent[];
disabled: boolean; disabled: boolean;
rootWidth: number;
rootHeight: number;
} }
export const CalendarDay = ({ date, events, disabled }: CalendarDayProps) => { export const CalendarDay = ({ date, events, disabled, rootHeight, rootWidth }: CalendarDayProps) => {
const [opened, setOpend] = useState(false); const [opened, setOpened] = useState(false);
const { primaryColor } = useMantineTheme(); const { primaryColor } = useMantineTheme();
const board = useRequiredBoard();
const mantineTheme = useMantineTheme();
const actualItemRadius = mantineTheme.radius[board.itemRadius];
const minAxisSize = Math.min(rootWidth, rootHeight);
const shouldScaleDown = minAxisSize < 350;
return ( return (
<Popover <Popover
@@ -25,7 +34,7 @@ export const CalendarDay = ({ date, events, disabled }: CalendarDayProps) => {
transitionProps={{ transitionProps={{
transition: "pop", transition: "pop",
}} }}
onChange={setOpend} onChange={setOpened}
opened={opened} opened={opened}
disabled={disabled} disabled={disabled}
> >
@@ -35,30 +44,23 @@ export const CalendarDay = ({ date, events, disabled }: CalendarDayProps) => {
w="100%" w="100%"
p={0} p={0}
m={0} m={0}
bd={`1cqmin solid ${opened && !disabled ? primaryColor : "transparent"}`} bd={`3px solid ${opened && !disabled ? primaryColor : "transparent"}`}
pos={"relative"}
style={{ style={{
alignContent: "center", alignContent: "center",
borderRadius: "3.5cqmin", borderRadius: actualItemRadius,
cursor: disabled ? "default" : "pointer", cursor: disabled ? "default" : "pointer",
}} }}
onClick={() => { onClick={() => {
if (disabled) return; if (disabled) return;
setOpend((prev) => !prev); setOpened((prev) => !prev);
}} }}
> >
<div <Text ta={"center"} size={shouldScaleDown ? "xs" : "md"} lh={1}>
style={{
textAlign: "center",
whiteSpace: "nowrap",
fontSize: "5cqmin",
lineHeight: "5cqmin",
paddingTop: "1.25cqmin",
}}
>
{date.getDate()} {date.getDate()}
</div> </Text>
<NotificationIndicator events={events} /> {rootHeight >= 350 && <NotificationIndicator events={events} />}
</Container> </Container>
</Popover.Target> </Popover.Target>
<Popover.Dropdown> <Popover.Dropdown>
@@ -75,19 +77,10 @@ interface NotificationIndicatorProps {
const NotificationIndicator = ({ events }: NotificationIndicatorProps) => { const NotificationIndicator = ({ events }: NotificationIndicatorProps) => {
const notificationEvents = [...new Set(events.map((event) => event.links[0]?.notificationColor))].filter(String); const notificationEvents = [...new Set(events.map((event) => event.links[0]?.notificationColor))].filter(String);
return ( return (
<Container h="0.7cqmin" w="80%" display="flex" p={0} style={{ flexDirection: "row", justifyContent: "center" }}> <Flex h="xs" w="75%" pos={"absolute"} bottom={0} left={"12.5%"} p={0} direction={"row"} justify={"center"}>
{notificationEvents.map((notificationEvent) => { {notificationEvents.map((notificationEvent) => {
return ( return <Box key={notificationEvent} bg={notificationEvent} h={4} p={0} style={{ flex: 1, borderRadius: 5 }} />;
<Container
key={notificationEvent}
bg={notificationEvent}
h="100%"
mx="0.25cqmin"
p={0}
style={{ flex: 1, borderRadius: "1000px" }}
/>
);
})} })}
</Container> </Flex>
); );
}; };

View File

@@ -2,11 +2,14 @@
import { useState } from "react"; import { useState } from "react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { useMantineTheme } from "@mantine/core";
import { Calendar } from "@mantine/dates"; import { Calendar } from "@mantine/dates";
import { useElementSize } from "@mantine/hooks";
import dayjs from "dayjs"; import dayjs from "dayjs";
import type { RouterOutputs } from "@homarr/api"; import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
import { useRequiredBoard } from "@homarr/boards/context";
import type { CalendarEvent } from "@homarr/integrations/types"; import type { CalendarEvent } from "@homarr/integrations/types";
import { useSettings } from "@homarr/settings"; import { useSettings } from "@homarr/settings";
@@ -60,6 +63,10 @@ const CalendarBase = ({ isEditMode, events, month, setMonth, options }: Calendar
const params = useParams(); const params = useParams();
const locale = params.locale as string; const locale = params.locale as string;
const { firstDayOfWeek } = useSettings(); const { firstDayOfWeek } = useSettings();
const board = useRequiredBoard();
const mantineTheme = useMantineTheme();
const actualItemRadius = mantineTheme.radius[board.itemRadius];
const { ref, width, height } = useElementSize();
return ( return (
<Calendar <Calendar
@@ -72,43 +79,39 @@ const CalendarBase = ({ isEditMode, events, month, setMonth, options }: Calendar
date={month} date={month}
maxLevel="month" maxLevel="month"
firstDayOfWeek={firstDayOfWeek} firstDayOfWeek={firstDayOfWeek}
w="100%"
h="100%"
static={isEditMode} static={isEditMode}
className={classes.calendar} className={classes.calendar}
w={"100%"}
h={"100%"}
ref={ref}
styles={{ styles={{
calendarHeaderControl: { calendarHeaderControl: {
pointerEvents: isEditMode ? "none" : undefined, pointerEvents: isEditMode ? "none" : undefined,
height: "12cqmin", borderRadius: "md",
width: "12cqmin",
borderRadius: "3.5cqmin",
}, },
calendarHeaderLevel: { calendarHeaderLevel: {
height: "12cqmin",
fontSize: "6cqmin",
pointerEvents: "none", pointerEvents: "none",
}, },
levelsGroup: { levelsGroup: {
height: "100%", height: "100%",
padding: "2.5cqmin", padding: "md",
}, },
calendarHeader: { calendarHeader: {
maxWidth: "unset", maxWidth: "unset",
marginBottom: 0, marginBottom: 0,
}, },
day: {
width: "12cqmin",
height: "12cqmin",
borderRadius: "3.5cqmin",
},
monthCell: { monthCell: {
textAlign: "center", textAlign: "center",
}, },
day: {
borderRadius: actualItemRadius,
width: width < 350 ? 20 : 50,
height: height < 350 ? 20 : 50,
},
month: { month: {
height: "100%", height: "100%",
}, },
weekday: { weekday: {
fontSize: "5.5cqmin",
padding: 0, padding: 0,
}, },
}} }}
@@ -122,7 +125,13 @@ const CalendarBase = ({ isEditMode, events, month, setMonth, options }: Calendar
})) }))
.filter((event): event is CalendarEvent => Boolean(event.date)); .filter((event): event is CalendarEvent => Boolean(event.date));
return ( return (
<CalendarDay date={tileDate} events={eventsForDate} disabled={isEditMode || eventsForDate.length === 0} /> <CalendarDay
date={tileDate}
events={eventsForDate}
disabled={isEditMode || eventsForDate.length === 0}
rootWidth={width}
rootHeight={height}
/>
); );
}} }}
/> />

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { Stack, Text } from "@mantine/core"; import { Stack, Text, Title } from "@mantine/core";
import dayjs from "dayjs"; import dayjs from "dayjs";
import advancedFormat from "dayjs/plugin/advancedFormat"; import advancedFormat from "dayjs/plugin/advancedFormat";
import timezones from "dayjs/plugin/timezone"; import timezones from "dayjs/plugin/timezone";
@@ -22,19 +22,19 @@ export default function ClockWidget({ options }: WidgetComponentProps<"clock">)
const timezone = options.useCustomTimezone ? options.timezone : Intl.DateTimeFormat().resolvedOptions().timeZone; const timezone = options.useCustomTimezone ? options.timezone : Intl.DateTimeFormat().resolvedOptions().timeZone;
const time = useCurrentTime(options); const time = useCurrentTime(options);
return ( return (
<Stack className="clock-text-stack" h="100%" align="center" justify="center" gap="10cqmin"> <Stack className="clock-text-stack" h="100%" align="center" justify="center" gap="sm">
{options.customTitleToggle && ( {options.customTitleToggle && (
<Text className="clock-customTitle-text" size="12.5cqmin" ta="center"> <Text className="clock-customTitle-text" size="md" ta="center">
{options.customTitle} {options.customTitle}
</Text> </Text>
)} )}
<Text className="clock-time-text" fw={700} size="22.5cqmin" lh="1"> <Title className="clock-time-text" fw={700} order={1} lh="1">
{options.customTimeFormat {options.customTimeFormat
? dayjs(time).tz(timezone).format(customTimeFormat) ? dayjs(time).tz(timezone).format(customTimeFormat)
: dayjs(time).tz(timezone).format(timeFormat)} : dayjs(time).tz(timezone).format(timeFormat)}
</Text> </Title>
{options.showDate && ( {options.showDate && (
<Text className="clock-date-text" size="12.5cqmin" p="1cqmin" lineClamp={1}> <Text className="clock-date-text" size="md" p="sm" lineClamp={1}>
{options.customDateFormat {options.customDateFormat
? dayjs(time).tz(timezone).format(customDateFormat) ? dayjs(time).tz(timezone).format(customDateFormat)
: dayjs(time).tz(timezone).format(dateFormat)} : dayjs(time).tz(timezone).format(dateFormat)}

View File

@@ -0,0 +1,7 @@
[data-mantine-color-scheme="light"] .card {
background-color: var(--mantine-color-gray-1);
}
[data-mantine-color-scheme="dark"] .card {
background-color: var(--mantine-color-dark-7);
}

View File

@@ -6,6 +6,7 @@ import { useState } from "react";
import { ActionIcon, Badge, Button, Card, Flex, ScrollArea, Stack, Text, Tooltip, UnstyledButton } from "@mantine/core"; import { ActionIcon, Badge, Button, Card, Flex, ScrollArea, Stack, Text, Tooltip, UnstyledButton } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks"; import { useDisclosure } from "@mantine/hooks";
import { IconCircleFilled, IconClockPause, IconPlayerPlay, IconPlayerStop } from "@tabler/icons-react"; import { IconCircleFilled, IconClockPause, IconPlayerPlay, IconPlayerStop } from "@tabler/icons-react";
import combineClasses from "clsx";
import type { RouterOutputs } from "@homarr/api"; import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
@@ -19,6 +20,7 @@ import { MaskedOrNormalImage } from "@homarr/ui";
import type { widgetKind } from "."; import type { widgetKind } from ".";
import type { WidgetComponentProps } from "../../definition"; import type { WidgetComponentProps } from "../../definition";
import classes from "./component.module.css";
import TimerModal from "./TimerModal"; import TimerModal from "./TimerModal";
const dnsLightStatus = (enabled: boolean | undefined) => const dnsLightStatus = (enabled: boolean | undefined) =>
@@ -179,12 +181,12 @@ export default function DnsHoleControlsWidget({
className="dns-hole-controls-stack" className="dns-hole-controls-stack"
h="100%" h="100%"
direction="column" direction="column"
p="2.5cqmin" p="sm"
gap="2.5cqmin" gap="sm"
style={{ pointerEvents: isEditMode ? "none" : undefined }} style={{ pointerEvents: isEditMode ? "none" : undefined }}
> >
{controlAllButtonsVisible && ( {controlAllButtonsVisible && (
<Flex className="dns-hole-controls-buttons" gap="2.5cqmin"> <Flex className="dns-hole-controls-buttons" gap="sm">
<Tooltip label={t("widget.dnsHoleControls.controls.enableAll")}> <Tooltip label={t("widget.dnsHoleControls.controls.enableAll")}>
<Button <Button
className="dns-hole-controls-enable-all-button" className="dns-hole-controls-enable-all-button"
@@ -193,15 +195,12 @@ export default function DnsHoleControlsWidget({
variant="light" variant="light"
color="green" color="green"
h="fit-content" h="fit-content"
p="1.25cqmin" p="xs"
bd={0} bd={0}
radius="2.5cqmin" radius={board.itemRadius}
flex={1} flex={1}
> >
<IconPlayerPlay <IconPlayerPlay className="dns-hole-controls-enable-all-icon" size={24} />
className="dns-hole-controls-enable-all-icon"
style={{ height: "7.5cqmin", width: "7.5cqmin" }}
/>
</Button> </Button>
</Tooltip> </Tooltip>
@@ -216,15 +215,12 @@ export default function DnsHoleControlsWidget({
variant="light" variant="light"
color="yellow" color="yellow"
h="fit-content" h="fit-content"
p="1.25cqmin" p="xs"
bd={0} bd={0}
radius="2.5cqmin" radius={board.itemRadius}
flex={1} flex={1}
> >
<IconClockPause <IconClockPause className="dns-hole-controls-timer-all-icon" size={24} />
className="dns-hole-controls-timer-all-icon"
style={{ height: "7.5cqmin", width: "7.5cqmin" }}
/>
</Button> </Button>
</Tooltip> </Tooltip>
@@ -236,15 +232,12 @@ export default function DnsHoleControlsWidget({
variant="light" variant="light"
color="red" color="red"
h="fit-content" h="fit-content"
p="1.25cqmin" p="xs"
bd={0} bd={0}
radius="2.5cqmin" radius={board.itemRadius}
flex={1} flex={1}
> >
<IconPlayerStop <IconPlayerStop className="dns-hole-controls-disable-all-icon" size={24} />
className="dns-hole-controls-disable-all-icon"
style={{ height: "7.5cqmin", width: "7.5cqmin" }}
/>
</Button> </Button>
</Tooltip> </Tooltip>
</Flex> </Flex>
@@ -253,7 +246,7 @@ export default function DnsHoleControlsWidget({
<ScrollArea className="dns-hole-controls-integration-list-scroll-area flexed-scroll-area"> <ScrollArea className="dns-hole-controls-integration-list-scroll-area flexed-scroll-area">
<Stack <Stack
className="dns-hole-controls-integration-list" className="dns-hole-controls-integration-list"
gap="2.5cqmin" gap="sm"
flex={1} flex={1}
justify={controlAllButtonsVisible ? "flex-end" : "space-evenly"} justify={controlAllButtonsVisible ? "flex-end" : "space-evenly"}
> >
@@ -306,34 +299,40 @@ const ControlsCard: React.FC<ControlsCardProps> = ({
const isInteractPermitted = integrationsWithInteractions.includes(data.integration.id); const isInteractPermitted = integrationsWithInteractions.includes(data.integration.id);
// Use all factors to infer the state of the action buttons // Use all factors to infer the state of the action buttons
const controlEnabled = isInteractPermitted && isEnabled !== undefined && isConnected; const controlEnabled = isInteractPermitted && isEnabled !== undefined && isConnected;
const board = useRequiredBoard();
const iconUrl = integrationDefs[data.integration.kind].iconUrl; const iconUrl = integrationDefs[data.integration.kind].iconUrl;
return ( return (
<Card <Card
className={`dns-hole-controls-integration-item-outer-shell dns-hole-controls-integration-item-${data.integration.id} dns-hole-controls-integration-item-${data.integration.name}`} className={combineClasses(
"dns-hole-controls-integration-item-outer-shell",
`dns-hole-controls-integration-item-${data.integration.id}`,
`dns-hole-controls-integration-item-${data.integration.name}`,
classes.card,
)}
key={data.integration.id} key={data.integration.id}
withBorder p="sm"
p="2.5cqmin" py={8}
radius="2.5cqmin" radius={board.itemRadius}
> >
<Flex className="dns-hole-controls-item-container" gap="4cqmin" align="center" direction="row"> <Flex className="dns-hole-controls-item-container" gap="md" align="center" direction="row">
<MaskedOrNormalImage <MaskedOrNormalImage
imageUrl={iconUrl} imageUrl={iconUrl}
hasColor={hasIconColor} hasColor={hasIconColor}
alt={data.integration.name} alt={data.integration.name}
className="dns-hole-controls-item-icon" className="dns-hole-controls-item-icon"
style={{ style={{
height: "20cqmin", height: 30,
width: "20cqmin", width: 30,
filter: !isConnected ? "grayscale(100%)" : undefined, filter: !isConnected ? "grayscale(100%)" : undefined,
}} }}
/> />
<Flex className="dns-hole-controls-item-data-stack" direction="column" gap="1.5cqmin"> <Flex className="dns-hole-controls-item-data-stack" direction="column" gap={5}>
<Text className="dns-hole-controls-item-integration-name" fz="7cqmin"> <Text className="dns-hole-controls-item-integration-name" fz="md" fw={"bold"}>
{data.integration.name} {data.integration.name}
</Text> </Text>
<Flex className="dns-hole-controls-item-controls" direction="row" gap="1.5cqmin"> <Flex className="dns-hole-controls-item-controls" direction="row" gap="lg">
<UnstyledButton <UnstyledButton
className="dns-hole-controls-item-toggle-button" className="dns-hole-controls-item-toggle-button"
disabled={!controlEnabled} disabled={!controlEnabled}
@@ -343,20 +342,18 @@ const ControlsCard: React.FC<ControlsCardProps> = ({
> >
<Badge <Badge
className={`dns-hole-controls-item-toggle-button-styling${controlEnabled ? " hoverable-component clickable-component" : ""}`} className={`dns-hole-controls-item-toggle-button-styling${controlEnabled ? " hoverable-component clickable-component" : ""}`}
bd="0.1cqmin solid var(--border-color)" bd="1px solid var(--border-color)"
px="2.5cqmin" px="sm"
h="7.5cqmin" h="lg"
fz="4.5cqmin"
lts="0.1cqmin"
color="var(--background-color)" color="var(--background-color)"
c="var(--mantine-color-text)" c="var(--mantine-color-text)"
styles={{ section: { marginInlineEnd: "2.5cqmin" }, root: { cursor: "inherit" } }} styles={{ section: { marginInlineEnd: "sm" }, root: { cursor: "inherit" } }}
leftSection={ leftSection={
isConnected && ( isConnected && (
<IconCircleFilled <IconCircleFilled
className="dns-hole-controls-item-status-icon" className="dns-hole-controls-item-status-icon"
color={dnsLightStatus(isEnabled)} color={dnsLightStatus(isEnabled)}
style={{ height: "3.5cqmin", width: "3.5cqmin" }} size={16}
/> />
) )
} }
@@ -374,27 +371,25 @@ const ControlsCard: React.FC<ControlsCardProps> = ({
)} )}
</Badge> </Badge>
</UnstyledButton> </UnstyledButton>
<ActionIcon
className="dns-hole-controls-item-timer-button"
display={isInteractPermitted ? undefined : "none"}
disabled={!controlEnabled || !isEnabled}
color="yellow"
size="fit-content"
radius="999px 999px 0px 999px"
bd={0}
variant="subtle"
onClick={() => {
setSelectedIntegrationIds([data.integration.id]);
open();
}}
>
<IconClockPause
className="dns-hole-controls-item-timer-icon"
style={{ height: "7.5cqmin", width: "7.5cqmin" }}
/>
</ActionIcon>
</Flex> </Flex>
</Flex> </Flex>
<ActionIcon
className="dns-hole-controls-item-timer-button"
display={isInteractPermitted ? undefined : "none"}
disabled={!controlEnabled || !isEnabled}
color="yellow"
size={30}
radius={board.itemRadius}
bd={0}
ms={"auto"}
variant="subtle"
onClick={() => {
setSelectedIntegrationIds([data.integration.id]);
open();
}}
>
<IconClockPause className="dns-hole-controls-item-timer-icon" size={20} />
</ActionIcon>
</Flex> </Flex>
</Card> </Card>
); );

View File

@@ -2,11 +2,12 @@
import { useMemo } from "react"; import { useMemo } from "react";
import type { BoxProps } from "@mantine/core"; import type { BoxProps } from "@mantine/core";
import { Avatar, AvatarGroup, Box, Card, Flex, Stack, Text, Tooltip, TooltipFloating } from "@mantine/core"; import { Avatar, AvatarGroup, Card, Flex, SimpleGrid, Stack, Text, Tooltip, TooltipFloating } from "@mantine/core";
import { useElementSize } from "@mantine/hooks"; import { useElementSize } from "@mantine/hooks";
import { IconBarrierBlock, IconPercentage, IconSearch, IconWorldWww } from "@tabler/icons-react"; import { IconBarrierBlock, IconPercentage, IconSearch, IconWorldWww } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
import { useRequiredBoard } from "@homarr/boards/context";
import { formatNumber } from "@homarr/common"; import { formatNumber } from "@homarr/common";
import { integrationDefs } from "@homarr/definitions"; import { integrationDefs } from "@homarr/definitions";
import type { DnsHoleSummary } from "@homarr/integrations/types"; import type { DnsHoleSummary } from "@homarr/integrations/types";
@@ -62,26 +63,26 @@ export default function DnsHoleSummaryWidget({ options, integrationIds }: Widget
const data = useMemo(() => summaries.flatMap(({ summary }) => summary), [summaries]); const data = useMemo(() => summaries.flatMap(({ summary }) => summary), [summaries]);
return ( return (
<Box h="100%" {...boxPropsByLayout(options.layout)} p="2cqmin"> <SimpleGrid cols={2} h="100%" p={"xs"} {...boxPropsByLayout(options.layout)}>
{data.length > 0 ? ( {data.length > 0 ? (
stats.map((item) => ( stats.map((item) => (
<StatCard key={item.color} item={item} usePiHoleColors={options.usePiHoleColors} data={data} t={t} /> <StatCard key={item.color} item={item} usePiHoleColors={options.usePiHoleColors} data={data} t={t} />
)) ))
) : ( ) : (
<Stack h="100%" w="100%" justify="center" align="center" gap="2.5cqmin" p="2.5cqmin"> <Stack h="100%" w="100%" justify="center" align="center" gap="sm" p="sm">
<AvatarGroup spacing="10cqmin"> <AvatarGroup spacing="md">
{summaries.map(({ integration }) => ( {summaries.map(({ integration }) => (
<Tooltip key={integration.id} label={integration.name}> <Tooltip key={integration.id} label={integration.name}>
<Avatar h="35cqmin" w="35cqmin" src={integrationDefs[integration.kind].iconUrl} /> <Avatar h={30} w={30} src={integrationDefs[integration.kind].iconUrl} />
</Tooltip> </Tooltip>
))} ))}
</AvatarGroup> </AvatarGroup>
<Text fz="10cqmin" ta="center"> <Text fz="md" ta="center">
{t("widget.dnsHoleSummary.error.integrationsDisconnected")} {t("widget.dnsHoleSummary.error.integrationsDisconnected")}
</Text> </Text>
</Stack> </Stack>
)} )}
</Box> </SimpleGrid>
); );
} }
@@ -152,30 +153,30 @@ const StatCard = ({ item, data, usePiHoleColors, t }: StatCardProps) => {
const { ref, height, width } = useElementSize(); const { ref, height, width } = useElementSize();
const isLong = width > height + 20; const isLong = width > height + 20;
const tooltip = item.tooltip?.(data, t); const tooltip = item.tooltip?.(data, t);
const board = useRequiredBoard();
return ( return (
<TooltipFloating label={tooltip} disabled={!tooltip} w={250} multiline> <TooltipFloating label={tooltip} disabled={!tooltip} w={250} multiline>
<Card <Card
ref={ref} ref={ref}
className="summary-card" className="summary-card"
m="2cqmin" p="sm"
p="2.5cqmin" radius={board.itemRadius}
bg={usePiHoleColors ? item.color : "rgba(96, 96, 96, 0.1)"} bg={usePiHoleColors ? item.color : "rgba(96, 96, 96, 0.1)"}
style={{ style={{
flex: 1, flex: 1,
}} }}
withBorder
> >
<Flex <Flex
className="summary-card-elements" className="summary-card-elements"
h="100%" h="100%"
w="100%" w="100%"
align="center" align="center"
justify="space-evenly" justify="center"
direction={isLong ? "row" : "column"} direction={isLong ? "row" : "column"}
style={{ containerType: "size" }} style={{ containerType: "size" }}
> >
<item.icon className="summary-card-icon" size="40cqmin" style={{ margin: "2.5cqmin" }} /> <item.icon className="summary-card-icon" size={50} />
<Flex <Flex
className="summary-card-texts" className="summary-card-texts"
justify="center" justify="center"
@@ -183,22 +184,15 @@ const StatCard = ({ item, data, usePiHoleColors, t }: StatCardProps) => {
style={{ style={{
flex: isLong ? 1 : undefined, flex: isLong ? 1 : undefined,
}} }}
mt={"xs"}
w="100%" w="100%"
h="100%" gap={0}
gap="1cqmin"
> >
<Text <Text key={item.value(data)} className="summary-card-value text-flash" ta="center" size="lg" fw="bold">
key={item.value(data)}
className="summary-card-value text-flash"
ta="center"
size="20cqmin"
fw="bold"
style={{ "--glow-size": "2.5cqmin" }}
>
{item.value(data)} {item.value(data)}
</Text> </Text>
{item.label && ( {item.label && (
<Text className="summary-card-label" ta="center" size="15cqmin"> <Text className="summary-card-label" ta="center" size="md">
{translateIfNecessary(t, item.label)} {translateIfNecessary(t, item.label)}
</Text> </Text>
)} )}

View File

@@ -675,7 +675,7 @@ export default function DownloadClientsWidget({
<Stack gap={0} h="100%" display="flex" style={baseStyle}> <Stack gap={0} h="100%" display="flex" style={baseStyle}>
<MantineReactTable table={table} /> <MantineReactTable table={table} />
<Group <Group
h="var(--ratio-width)" h={40}
px="var(--space-size)" px="var(--space-size)"
justify={integrationTypes.includes("torrent") ? "space-between" : "end"} justify={integrationTypes.includes("torrent") ? "space-between" : "end"}
style={{ style={{
@@ -690,7 +690,6 @@ export default function DownloadClientsWidget({
)} )}
<ClientsControl <ClientsControl
clients={clients} clients={clients}
style={editStyle}
filters={quickFilters} filters={quickFilters}
setFilters={setQuickFilters} setFilters={setQuickFilters}
availableStatuses={availableStatuses} availableStatuses={availableStatuses}
@@ -785,10 +784,9 @@ interface ClientsControlProps {
filters: QuickFilter; filters: QuickFilter;
setFilters: (filters: QuickFilter) => void; setFilters: (filters: QuickFilter) => void;
availableStatuses: QuickFilter["statuses"]; availableStatuses: QuickFilter["statuses"];
style?: MantineStyleProp;
} }
const ClientsControl = ({ clients, filters, setFilters, availableStatuses, style }: ClientsControlProps) => { const ClientsControl = ({ clients, filters, setFilters, availableStatuses }: ClientsControlProps) => {
const integrationsStatuses = clients.reduce( const integrationsStatuses = clients.reduce(
(acc, { status, integration: { id }, interact }) => (acc, { status, integration: { id }, interact }) =>
status && interact ? (acc[status.paused ? "paused" : "active"].push(id), acc) : acc, status && interact ? (acc[status.paused ? "paused" : "active"].push(id), acc) : acc,
@@ -799,33 +797,21 @@ const ClientsControl = ({ clients, filters, setFilters, availableStatuses, style
clients.reduce((count, { status }) => count + (status?.rates.down ?? 0), 0), clients.reduce((count, { status }) => count + (status?.rates.down ?? 0), 0),
"/s", "/s",
); );
const chipStyle = {
"--chip-fz": "var(--button-fz)",
"--chip-size": "calc(var(--ratio-width) * 0.9)",
"--chip-icon-size": "calc(var(--chip-fz)*2/3)",
"--chip-padding": "var(--chip-fz)",
"--chip-checked-padding": "var(--chip-icon-size)",
"--chip-spacing": "var(--space-size)",
};
const { mutate: mutateResumeQueue } = clientApi.widget.downloads.resume.useMutation(); const { mutate: mutateResumeQueue } = clientApi.widget.downloads.resume.useMutation();
const { mutate: mutatePauseQueue } = clientApi.widget.downloads.pause.useMutation(); const { mutate: mutatePauseQueue } = clientApi.widget.downloads.pause.useMutation();
const [opened, { open, close }] = useDisclosure(false); const [opened, { open, close }] = useDisclosure(false);
const t = useScopedI18n("widget.downloads"); const t = useScopedI18n("widget.downloads");
return ( return (
<Group gap="var(--space-size)" style={style}> <Group gap={5}>
<Popover withinPortal={false} offset={0}> <Popover withinPortal={false} offset={0}>
<Popover.Target> <Popover.Target>
<ActionIcon size="var(--button-size)" radius={999} variant="light"> <ActionIcon size={30} radius={999} variant="light">
<IconFilter style={actionIconIconStyle} /> <IconFilter style={actionIconIconStyle} />
</ActionIcon> </ActionIcon>
</Popover.Target> </Popover.Target>
<Popover.Dropdown <Popover.Dropdown>
w="calc(var(--ratio-width)*4)" <Stack gap="md" align="center" pb="var(--space-size)">
p="var(--space-size)"
bg="var(--background-color)"
style={{ "--popover-border-color": "var(--border-color)" }}
>
<Stack gap="var(--space-size)" align="center" pb="var(--space-size)">
<Text fw="700">{t("items.integration.columnTitle")}</Text> <Text fw="700">{t("items.integration.columnTitle")}</Text>
<Chip.Group <Chip.Group
multiple multiple
@@ -833,7 +819,7 @@ const ClientsControl = ({ clients, filters, setFilters, availableStatuses, style
onChange={(names) => setFilters({ ...filters, integrationKinds: names })} onChange={(names) => setFilters({ ...filters, integrationKinds: names })}
> >
{clients.map(({ integration }) => ( {clients.map(({ integration }) => (
<Chip style={chipStyle} key={integration.id} value={integration.name}> <Chip key={integration.id} value={integration.name}>
{integration.name} {integration.name}
</Chip> </Chip>
))} ))}
@@ -845,7 +831,7 @@ const ClientsControl = ({ clients, filters, setFilters, availableStatuses, style
onChange={(statuses) => setFilters({ ...filters, statuses: statuses as typeof filters.statuses })} onChange={(statuses) => setFilters({ ...filters, statuses: statuses as typeof filters.statuses })}
> >
{availableStatuses.map((status) => ( {availableStatuses.map((status) => (
<Chip style={chipStyle} key={status} value={status}> <Chip key={status} value={status}>
{t(`states.${status}`)} {t(`states.${status}`)}
</Chip> </Chip>
))} ))}
@@ -861,7 +847,7 @@ const ClientsControl = ({ clients, filters, setFilters, availableStatuses, style
{someInteract && ( {someInteract && (
<Tooltip label={t("actions.clients.resume")}> <Tooltip label={t("actions.clients.resume")}>
<ActionIcon <ActionIcon
size="var(--button-size)" size={30}
radius={999} radius={999}
disabled={integrationsStatuses.paused.length === 0} disabled={integrationsStatuses.paused.length === 0}
variant="light" variant="light"
@@ -885,7 +871,7 @@ const ClientsControl = ({ clients, filters, setFilters, availableStatuses, style
{someInteract && ( {someInteract && (
<Tooltip label={t("actions.clients.pause")}> <Tooltip label={t("actions.clients.pause")}>
<ActionIcon <ActionIcon
size="var(--button-size)" size={30}
radius={999} radius={999}
disabled={integrationsStatuses.active.length === 0} disabled={integrationsStatuses.active.length === 0}
variant="light" variant="light"
@@ -981,8 +967,8 @@ const ClientAvatar = ({ client }: ClientAvatarProps) => {
key={client.integration.id} key={client.integration.id}
src={getIconUrl(client.integration.kind)} src={getIconUrl(client.integration.kind)}
style={{ filter: !isConnected ? "grayscale(100%)" : undefined }} style={{ filter: !isConnected ? "grayscale(100%)" : undefined }}
size="var(--image-size)" size={30}
p="calc(var(--space-size)*0.5)" p={5}
bd={`calc(var(--space-size)*0.5) solid ${client.status ? "transparent" : "var(--mantine-color-red-filled)"}`} bd={`calc(var(--space-size)*0.5) solid ${client.status ? "transparent" : "var(--mantine-color-red-filled)"}`}
/> />
); );

View File

@@ -0,0 +1,33 @@
import { Box, Center, RingProgress, Text } from "@mantine/core";
import { useElementSize } from "@mantine/hooks";
import { IconCpu } from "@tabler/icons-react";
import { progressColor } from "../system-health";
export const CpuRing = ({ cpuUtilization }: { cpuUtilization: number }) => {
const { width, ref } = useElementSize();
const fallbackWidth = width || 1; // See https://github.com/homarr-labs/homarr/issues/2196
return (
<Box ref={ref} w="100%" h="100%" className="health-monitoring-cpu">
<RingProgress
className="health-monitoring-cpu-utilization"
roundCaps
size={fallbackWidth * 0.95}
thickness={fallbackWidth / 10}
label={
<Center style={{ flexDirection: "column" }}>
<Text className="health-monitoring-cpu-utilization-value" size="sm">{`${cpuUtilization.toFixed(2)}%`}</Text>
<IconCpu className="health-monitoring-cpu-utilization-icon" size={30} />
</Center>
}
sections={[
{
value: Number(cpuUtilization.toFixed(2)),
color: progressColor(Number(cpuUtilization.toFixed(2))),
},
]}
/>
</Box>
);
};

View File

@@ -0,0 +1,39 @@
import { Box, Center, RingProgress, Text } from "@mantine/core";
import { useElementSize } from "@mantine/hooks";
import { IconCpu } from "@tabler/icons-react";
import { progressColor } from "../system-health";
export const CpuTempRing = ({ fahrenheit, cpuTemp }: { fahrenheit: boolean; cpuTemp: number | undefined }) => {
const { width, ref } = useElementSize();
const fallbackWidth = width || 1; // See https://github.com/homarr-labs/homarr/issues/2196
if (!cpuTemp) {
return null;
}
return (
<Box ref={ref} w="100%" h="100%" className="health-monitoring-cpu-temperature">
<RingProgress
className="health-monitoring-cpu-temp"
roundCaps
size={fallbackWidth * 0.95}
thickness={fallbackWidth / 10}
label={
<Center style={{ flexDirection: "column" }}>
<Text className="health-monitoring-cpu-temp-value" size="sm">
{fahrenheit ? `${(cpuTemp * 1.8 + 32).toFixed(1)}°F` : `${cpuTemp.toFixed(1)}°C`}
</Text>
<IconCpu className="health-monitoring-cpu-temp-icon" size={30} />
</Center>
}
sections={[
{
value: cpuTemp,
color: progressColor(cpuTemp),
},
]}
/>
</Box>
);
};

View File

@@ -0,0 +1,54 @@
import { Box, Center, RingProgress, Text } from "@mantine/core";
import { useElementSize } from "@mantine/hooks";
import { IconBrain } from "@tabler/icons-react";
import { progressColor } from "../system-health";
export const MemoryRing = ({ available, used }: { available: string; used: string }) => {
const { width, ref } = useElementSize();
const fallbackWidth = width || 1; // See https://github.com/homarr-labs/homarr/issues/2196
const memoryUsage = formatMemoryUsage(available, used);
return (
<Box ref={ref} w="100%" h="100%" className="health-monitoring-memory">
<RingProgress
className="health-monitoring-memory-use"
roundCaps
size={fallbackWidth * 0.95}
thickness={fallbackWidth / 10}
label={
<Center style={{ flexDirection: "column" }}>
<Text className="health-monitoring-memory-value" size="sm">
{memoryUsage.memUsed.GB}GiB
</Text>
<IconBrain className="health-monitoring-memory-icon" size={30} />
</Center>
}
sections={[
{
value: Number(memoryUsage.memUsed.percent),
color: progressColor(Number(memoryUsage.memUsed.percent)),
tooltip: `${memoryUsage.memUsed.percent}%`,
},
]}
/>
</Box>
);
};
export const formatMemoryUsage = (memFree: string, memUsed: string) => {
const memFreeBytes = Number(memFree);
const memUsedBytes = Number(memUsed);
const totalMemory = memFreeBytes + memUsedBytes;
const memFreeGB = (memFreeBytes / 1024 ** 3).toFixed(2);
const memUsedGB = (memUsedBytes / 1024 ** 3).toFixed(2);
const memFreePercent = Math.round((memFreeBytes / totalMemory) * 100);
const memUsedPercent = Math.round((memUsedBytes / totalMemory) * 100);
const memTotalGB = (totalMemory / 1024 ** 3).toFixed(2);
return {
memFree: { percent: memFreePercent, GB: memFreeGB },
memUsed: { percent: memUsedPercent, GB: memUsedGB },
memTotal: { GB: memTotalGB },
};
};

View File

@@ -0,0 +1,7 @@
[data-mantine-color-scheme="light"] .card {
background-color: var(--mantine-color-gray-1);
}
[data-mantine-color-scheme="dark"] .card {
background-color: var(--mantine-color-dark-7);
}

View File

@@ -1,10 +1,9 @@
"use client"; "use client";
import { import {
Avatar, ActionIcon,
Box, Box,
Card, Card,
Center,
Divider, Divider,
Flex, Flex,
Group, Group,
@@ -12,12 +11,11 @@ import {
List, List,
Modal, Modal,
Progress, Progress,
RingProgress,
Stack, Stack,
Text, Text,
Tooltip, Tooltip,
} from "@mantine/core"; } from "@mantine/core";
import { useDisclosure, useElementSize } from "@mantine/hooks"; import { useDisclosure } from "@mantine/hooks";
import { import {
IconBrain, IconBrain,
IconClock, IconClock,
@@ -29,14 +27,20 @@ import {
IconTemperature, IconTemperature,
IconVersions, IconVersions,
} from "@tabler/icons-react"; } from "@tabler/icons-react";
import combineClasses from "clsx";
import dayjs from "dayjs"; import dayjs from "dayjs";
import duration from "dayjs/plugin/duration"; import duration from "dayjs/plugin/duration";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
import { useRequiredBoard } from "@homarr/boards/context";
import type { TranslationFunction } from "@homarr/translation"; import type { TranslationFunction } from "@homarr/translation";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import type { WidgetComponentProps } from "../definition"; import type { WidgetComponentProps } from "../definition";
import { CpuRing } from "./rings/cpu-ring";
import { CpuTempRing } from "./rings/cpu-temp-ring";
import { formatMemoryUsage, MemoryRing } from "./rings/memory-ring";
import classes from "./system-health.module.css";
dayjs.extend(duration); dayjs.extend(duration);
@@ -55,6 +59,7 @@ export const SystemHealthMonitoring = ({ options, integrationIds }: WidgetCompon
); );
const [opened, { open, close }] = useDisclosure(false); const [opened, { open, close }] = useDisclosure(false);
const utils = clientApi.useUtils(); const utils = clientApi.useUtils();
const board = useRequiredBoard();
clientApi.widget.healthMonitoring.subscribeSystemHealthStatus.useSubscription( clientApi.widget.healthMonitoring.subscribeSystemHealthStatus.useSubscription(
{ integrationIds }, { integrationIds },
@@ -75,23 +80,21 @@ export const SystemHealthMonitoring = ({ options, integrationIds }: WidgetCompon
); );
return ( return (
<Stack h="100%" gap="2.5cqmin" className="health-monitoring"> <Stack h="100%" gap="sm" className="health-monitoring">
{healthData.map(({ integrationId, integrationName, healthInfo, updatedAt }) => { {healthData.map(({ integrationId, integrationName, healthInfo, updatedAt }) => {
const disksData = matchFileSystemAndSmart(healthInfo.fileSystem, healthInfo.smart); const disksData = matchFileSystemAndSmart(healthInfo.fileSystem, healthInfo.smart);
const memoryUsage = formatMemoryUsage(healthInfo.memAvailable, healthInfo.memUsed); const memoryUsage = formatMemoryUsage(healthInfo.memAvailable, healthInfo.memUsed);
return ( return (
<Stack <Stack
gap="2.5cqmin" gap="sm"
key={integrationId} key={integrationId}
h="100%" h="100%"
className={`health-monitoring-information health-monitoring-${integrationName}`} className={`health-monitoring-information health-monitoring-${integrationName}`}
p="2.5cqmin" p="sm"
> >
<Card className="health-monitoring-information-card" p="2.5cqmin" withBorder> <Box className="health-monitoring-information-card" p="sm">
<Flex <Flex
className="health-monitoring-information-card-elements" className="health-monitoring-information-card-elements"
h="100%"
w="100%"
justify="space-between" justify="space-between"
align="center" align="center"
key={integrationId} key={integrationId}
@@ -103,13 +106,19 @@ export const SystemHealthMonitoring = ({ options, integrationIds }: WidgetCompon
processing processing
color={healthInfo.rebootRequired ? "red" : healthInfo.availablePkgUpdates > 0 ? "blue" : "gray"} color={healthInfo.rebootRequired ? "red" : healthInfo.availablePkgUpdates > 0 ? "blue" : "gray"}
position="top-end" position="top-end"
size="4cqmin" size="md"
label={healthInfo.availablePkgUpdates > 0 ? healthInfo.availablePkgUpdates : undefined} label={healthInfo.availablePkgUpdates > 0 ? healthInfo.availablePkgUpdates : undefined}
disabled={!healthInfo.rebootRequired && healthInfo.availablePkgUpdates === 0} disabled={!healthInfo.rebootRequired && healthInfo.availablePkgUpdates === 0}
> >
<Avatar className="health-monitoring-information-icon-avatar" size="10cqmin" radius="sm"> <ActionIcon
<IconInfoCircle className="health-monitoring-information-icon" size="8cqmin" onClick={open} /> className="health-monitoring-information-icon-avatar"
</Avatar> variant={"light"}
color="var(--mantine-color-text)"
size={40}
radius={board.itemRadius}
>
<IconInfoCircle className="health-monitoring-information-icon" size={30} onClick={open} />
</ActionIcon>
</Indicator> </Indicator>
<Modal <Modal
opened={opened} opened={opened}
@@ -120,49 +129,31 @@ export const SystemHealthMonitoring = ({ options, integrationIds }: WidgetCompon
> >
<Stack gap="10px" className="health-monitoring-modal-stack"> <Stack gap="10px" className="health-monitoring-modal-stack">
<Divider /> <Divider />
<List className="health-monitoring-information-list" center spacing="0.5cqmin"> <List className="health-monitoring-information-list" center spacing="xs">
<List.Item <List.Item className="health-monitoring-information-processor" icon={<IconCpu2 size={30} />}>
className="health-monitoring-information-processor"
icon={<IconCpu2 size="1.5cqmin" />}
>
{t("widget.healthMonitoring.popover.processor", { cpuModelName: healthInfo.cpuModelName })} {t("widget.healthMonitoring.popover.processor", { cpuModelName: healthInfo.cpuModelName })}
</List.Item> </List.Item>
<List.Item <List.Item className="health-monitoring-information-memory" icon={<IconBrain size={30} />}>
className="health-monitoring-information-memory"
icon={<IconBrain size="1.5cqmin" />}
>
{t("widget.healthMonitoring.popover.memory", { memory: memoryUsage.memTotal.GB })} {t("widget.healthMonitoring.popover.memory", { memory: memoryUsage.memTotal.GB })}
</List.Item> </List.Item>
<List.Item <List.Item className="health-monitoring-information-memory" icon={<IconBrain size={30} />}>
className="health-monitoring-information-memory"
icon={<IconBrain size="1.5cqmin" />}
>
{t("widget.healthMonitoring.popover.memoryAvailable", { {t("widget.healthMonitoring.popover.memoryAvailable", {
memoryAvailable: memoryUsage.memFree.GB, memoryAvailable: memoryUsage.memFree.GB,
percent: memoryUsage.memFree.percent, percent: memoryUsage.memFree.percent,
})} })}
</List.Item> </List.Item>
<List.Item <List.Item className="health-monitoring-information-version" icon={<IconVersions size={30} />}>
className="health-monitoring-information-version"
icon={<IconVersions size="1.5cqmin" />}
>
{t("widget.healthMonitoring.popover.version", { {t("widget.healthMonitoring.popover.version", {
version: healthInfo.version, version: healthInfo.version,
})} })}
</List.Item> </List.Item>
<List.Item <List.Item className="health-monitoring-information-uptime" icon={<IconClock size={30} />}>
className="health-monitoring-information-uptime"
icon={<IconClock size="1.5cqmin" />}
>
{formatUptime(healthInfo.uptime, t)} {formatUptime(healthInfo.uptime, t)}
</List.Item> </List.Item>
<List.Item <List.Item className="health-monitoring-information-load-average" icon={<IconCpu size={30} />}>
className="health-monitoring-information-load-average"
icon={<IconCpu size="1.5cqmin" />}
>
{t("widget.healthMonitoring.popover.loadAverage")} {t("widget.healthMonitoring.popover.loadAverage")}
</List.Item> </List.Item>
<List m="0.5cqmin" withPadding center spacing="0.5cqmin" icon={<IconCpu size="1cqmin" />}> <List m="xs" withPadding center spacing="xs" icon={<IconCpu size={30} />}>
<List.Item className="health-monitoring-information-load-average-1min"> <List.Item className="health-monitoring-information-load-average-1min">
{t("widget.healthMonitoring.popover.minute")} {healthInfo.loadAverage["1min"]}% {t("widget.healthMonitoring.popover.minute")} {healthInfo.loadAverage["1min"]}%
</List.Item> </List.Item>
@@ -184,56 +175,53 @@ export const SystemHealthMonitoring = ({ options, integrationIds }: WidgetCompon
{options.memory && <MemoryRing available={healthInfo.memAvailable} used={healthInfo.memUsed} />} {options.memory && <MemoryRing available={healthInfo.memAvailable} used={healthInfo.memUsed} />}
</Flex> </Flex>
{ {
<Text <Text className="health-monitoring-status-update-time" c="dimmed" size="sm" ta="center">
className="health-monitoring-status-update-time"
c="dimmed"
size="3.5cqmin"
ta="center"
mb="2.5cqmin"
>
{t("widget.healthMonitoring.popover.lastSeen", { lastSeen: dayjs(updatedAt).fromNow() })} {t("widget.healthMonitoring.popover.lastSeen", { lastSeen: dayjs(updatedAt).fromNow() })}
</Text> </Text>
} }
</Card> </Box>
{options.fileSystem && {options.fileSystem &&
disksData.map((disk) => { disksData.map((disk) => {
return ( return (
<Card <Card
className={`health-monitoring-disk-card health-monitoring-disk-card-${integrationName}`} className={combineClasses(
`health-monitoring-disk-card health-monitoring-disk-card-${integrationName}`,
classes.card,
)}
key={disk.deviceName} key={disk.deviceName}
p="2.5cqmin" radius={board.itemRadius}
withBorder p="sm"
> >
<Flex className="health-monitoring-disk-status" justify="space-between" align="center" m="1.5cqmin"> <Flex className="health-monitoring-disk-status" justify="space-between" align="center" mb="sm">
<Group gap="1cqmin"> <Group gap="xs">
<IconServer className="health-monitoring-disk-icon" size="5cqmin" /> <IconServer className="health-monitoring-disk-icon" size="1rem" />
<Text className="dihealth-monitoring-disk-name" size="4cqmin"> <Text className="dihealth-monitoring-disk-name" size={"md"}>
{disk.deviceName} {disk.deviceName}
</Text> </Text>
</Group> </Group>
<Group gap="1cqmin"> <Group gap="xs">
<IconTemperature className="health-monitoring-disk-temperature-icon" size="5cqmin" /> <IconTemperature className="health-monitoring-disk-temperature-icon" size="1rem" />
<Text className="health-monitoring-disk-temperature-value" size="4cqmin"> <Text className="health-monitoring-disk-temperature-value" size="md">
{options.fahrenheit {options.fahrenheit
? `${(disk.temperature * 1.8 + 32).toFixed(1)}°F` ? `${(disk.temperature * 1.8 + 32).toFixed(1)}°F`
: `${disk.temperature}°C`} : `${disk.temperature}°C`}
</Text> </Text>
</Group> </Group>
<Group gap="1cqmin"> <Group gap="xs">
<IconFileReport className="health-monitoring-disk-status-icon" size="5cqmin" /> <IconFileReport className="health-monitoring-disk-status-icon" size="1rem" />
<Text className="health-monitoring-disk-status-value" size="4cqmin"> <Text className="health-monitoring-disk-status-value" size="md">
{disk.overallStatus ? disk.overallStatus : "N/A"} {disk.overallStatus ? disk.overallStatus : "N/A"}
</Text> </Text>
</Group> </Group>
</Flex> </Flex>
<Progress.Root className="health-monitoring-disk-use" h="6cqmin"> <Progress.Root className="health-monitoring-disk-use" radius={board.itemRadius} h="md">
<Tooltip label={disk.used}> <Tooltip label={disk.used}>
<Progress.Section <Progress.Section
value={disk.percentage} value={disk.percentage}
color={progressColor(disk.percentage)} color={progressColor(disk.percentage)}
className="health-monitoring-disk-use-percentage" className="health-monitoring-disk-use-percentage"
> >
<Progress.Label className="health-monitoring-disk-use-value" fz="2.5cqmin"> <Progress.Label className="health-monitoring-disk-use-value" fz="xs">
{t("widget.healthMonitoring.popover.used")} {t("widget.healthMonitoring.popover.used")}
</Progress.Label> </Progress.Label>
</Progress.Section> </Progress.Section>
@@ -251,7 +239,7 @@ export const SystemHealthMonitoring = ({ options, integrationIds }: WidgetCompon
value={100 - disk.percentage} value={100 - disk.percentage}
color="default" color="default"
> >
<Progress.Label className="health-monitoring-disk-available-value" fz="2.5cqmin"> <Progress.Label className="health-monitoring-disk-available-value" fz="xs">
{t("widget.healthMonitoring.popover.available")} {t("widget.healthMonitoring.popover.available")}
</Progress.Label> </Progress.Label>
</Progress.Section> </Progress.Section>
@@ -314,117 +302,3 @@ export const matchFileSystemAndSmart = (fileSystems: FileSystem[], smartData: Sm
}) })
.sort((fileSystemA, fileSystemB) => fileSystemA.deviceName.localeCompare(fileSystemB.deviceName)); .sort((fileSystemA, fileSystemB) => fileSystemA.deviceName.localeCompare(fileSystemB.deviceName));
}; };
const CpuRing = ({ cpuUtilization }: { cpuUtilization: number }) => {
const { width, ref } = useElementSize();
const fallbackWidth = width || 1; // See https://github.com/homarr-labs/homarr/issues/2196
return (
<Box ref={ref} w="100%" h="100%" className="health-monitoring-cpu">
<RingProgress
className="health-monitoring-cpu-utilization"
roundCaps
size={fallbackWidth * 0.95}
thickness={fallbackWidth / 10}
label={
<Center style={{ flexDirection: "column" }}>
<Text
className="health-monitoring-cpu-utilization-value"
size="3cqmin"
>{`${cpuUtilization.toFixed(2)}%`}</Text>
<IconCpu className="health-monitoring-cpu-utilization-icon" size="7cqmin" />
</Center>
}
sections={[
{
value: Number(cpuUtilization.toFixed(2)),
color: progressColor(Number(cpuUtilization.toFixed(2))),
},
]}
/>
</Box>
);
};
const CpuTempRing = ({ fahrenheit, cpuTemp }: { fahrenheit: boolean; cpuTemp: number | undefined }) => {
const { width, ref } = useElementSize();
const fallbackWidth = width || 1; // See https://github.com/homarr-labs/homarr/issues/2196
if (!cpuTemp) {
return null;
}
return (
<Box ref={ref} w="100%" h="100%" className="health-monitoring-cpu-temperature">
<RingProgress
className="health-monitoring-cpu-temp"
roundCaps
size={fallbackWidth * 0.95}
thickness={fallbackWidth / 10}
label={
<Center style={{ flexDirection: "column" }}>
<Text className="health-monitoring-cpu-temp-value" size="3cqmin">
{fahrenheit ? `${(cpuTemp * 1.8 + 32).toFixed(1)}°F` : `${cpuTemp.toFixed(1)}°C`}
</Text>
<IconCpu className="health-monitoring-cpu-temp-icon" size="7cqmin" />
</Center>
}
sections={[
{
value: cpuTemp,
color: progressColor(cpuTemp),
},
]}
/>
</Box>
);
};
const MemoryRing = ({ available, used }: { available: string; used: string }) => {
const { width, ref } = useElementSize();
const fallbackWidth = width || 1; // See https://github.com/homarr-labs/homarr/issues/2196
const memoryUsage = formatMemoryUsage(available, used);
return (
<Box ref={ref} w="100%" h="100%" className="health-monitoring-memory">
<RingProgress
className="health-monitoring-memory-use"
roundCaps
size={fallbackWidth * 0.95}
thickness={fallbackWidth / 10}
label={
<Center style={{ flexDirection: "column" }}>
<Text className="health-monitoring-memory-value" size="3cqmin">
{memoryUsage.memUsed.GB}GiB
</Text>
<IconBrain className="health-monitoring-memory-icon" size="7cqmin" />
</Center>
}
sections={[
{
value: Number(memoryUsage.memUsed.percent),
color: progressColor(Number(memoryUsage.memUsed.percent)),
tooltip: `${memoryUsage.memUsed.percent}%`,
},
]}
/>
</Box>
);
};
export const formatMemoryUsage = (memFree: string, memUsed: string) => {
const memFreeBytes = Number(memFree);
const memUsedBytes = Number(memUsed);
const totalMemory = memFreeBytes + memUsedBytes;
const memFreeGB = (memFreeBytes / 1024 ** 3).toFixed(2);
const memUsedGB = (memUsedBytes / 1024 ** 3).toFixed(2);
const memFreePercent = Math.round((memFreeBytes / totalMemory) * 100);
const memUsedPercent = Math.round((memUsedBytes / totalMemory) * 100);
const memTotalGB = (totalMemory / 1024 ** 3).toFixed(2);
return {
memFree: { percent: memFreePercent, GB: memFreeGB },
memUsed: { percent: memUsedPercent, GB: memUsedGB },
memTotal: { GB: memTotalGB },
};
};

View File

@@ -0,0 +1,7 @@
[data-mantine-color-scheme="light"] .card {
background-color: var(--mantine-color-gray-1);
}
[data-mantine-color-scheme="dark"] .card {
background-color: var(--mantine-color-dark-7);
}

View File

@@ -2,11 +2,14 @@
import { Anchor, Button, Card, Container, Flex, Group, ScrollArea, Text } from "@mantine/core"; import { Anchor, Button, Card, Container, Flex, Group, ScrollArea, Text } from "@mantine/core";
import { IconCircleCheck, IconCircleX, IconReportSearch, IconTestPipe } from "@tabler/icons-react"; import { IconCircleCheck, IconCircleX, IconReportSearch, IconTestPipe } from "@tabler/icons-react";
import combineClasses from "clsx";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
import { useRequiredBoard } from "@homarr/boards/context";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import type { WidgetComponentProps } from "../definition"; import type { WidgetComponentProps } from "../definition";
import classes from "./component.module.css";
export default function IndexerManagerWidget({ options, integrationIds }: WidgetComponentProps<"indexerManager">) { export default function IndexerManagerWidget({ options, integrationIds }: WidgetComponentProps<"indexerManager">) {
const t = useI18n(); const t = useI18n();
@@ -22,6 +25,7 @@ export default function IndexerManagerWidget({ options, integrationIds }: Widget
const utils = clientApi.useUtils(); const utils = clientApi.useUtils();
const { mutate: testAll, isPending } = clientApi.widget.indexerManager.testAllIndexers.useMutation(); const { mutate: testAll, isPending } = clientApi.widget.indexerManager.testAllIndexers.useMutation();
const board = useRequiredBoard();
clientApi.widget.indexerManager.subscribeIndexersStatus.useSubscription( clientApi.widget.indexerManager.subscribeIndexersStatus.useSubscription(
{ integrationIds }, { integrationIds },
@@ -36,21 +40,28 @@ export default function IndexerManagerWidget({ options, integrationIds }: Widget
}, },
); );
const iconStyle = { height: "7.5cqmin", width: "7.5cqmin" };
return ( return (
<Flex className="indexer-manager-container" h="100%" direction="column" gap="2.5cqmin" p="2.5cqmin" align="center"> <Flex className="indexer-manager-container" h="100%" direction="column" gap="sm" p="sm" align="center">
<Text className="indexer-manager-title" size="6.5cqmin"> <Flex className="indexer-manager-title" align={"center"} gap={"xs"}>
<IconReportSearch className="indexer-manager-title-icon" size="7cqmin" /> {t("widget.indexerManager.title")} <IconReportSearch className="indexer-manager-title-icon" size={30} />
</Text> <Text size="md" fw={"bold"}>
<Card className="indexer-manager-list-container" w="100%" p="2.5cqmin" radius="md" flex={1} withBorder> {t("widget.indexerManager.title")}
</Text>
</Flex>
<Card
className={combineClasses("indexer-manager-list-container", classes.card)}
w="100%"
p="sm"
radius={board.itemRadius}
flex={1}
>
<ScrollArea className="indexer-manager-list-scroll-area" h="100%"> <ScrollArea className="indexer-manager-list-scroll-area" h="100%">
{indexersData.map(({ integrationId, indexers }) => ( {indexersData.map(({ integrationId, indexers }) => (
<Container className={`indexer-manager-${integrationId}-list-container`} p={0} key={integrationId}> <Container className={`indexer-manager-${integrationId}-list-container`} p={0} key={integrationId}>
{indexers.map((indexer) => ( {indexers.map((indexer) => (
<Group <Group
className={`indexer-manager-line indexer-manager-${indexer.name}`} className={`indexer-manager-line indexer-manager-${indexer.name}`}
h="7.5cqmin" h={30}
key={indexer.id} key={indexer.id}
justify="space-between" justify="space-between"
> >
@@ -59,7 +70,7 @@ export default function IndexerManagerWidget({ options, integrationIds }: Widget
href={indexer.url} href={indexer.url}
target={options.openIndexerSiteInNewTab ? "_blank" : "_self"} target={options.openIndexerSiteInNewTab ? "_blank" : "_self"}
> >
<Text className="indexer-manager-line-anchor-text" c="dimmed" size="5cqmin"> <Text className="indexer-manager-line-anchor-text" c="dimmed" size="md">
{indexer.name} {indexer.name}
</Text> </Text>
</Anchor> </Anchor>
@@ -67,13 +78,13 @@ export default function IndexerManagerWidget({ options, integrationIds }: Widget
<IconCircleX <IconCircleX
className="indexer-manager-line-status-icon indexer-manager-line-icon-disabled" className="indexer-manager-line-status-icon indexer-manager-line-icon-disabled"
color="#d9534f" color="#d9534f"
style={iconStyle} size={24}
/> />
) : ( ) : (
<IconCircleCheck <IconCircleCheck
className="indexer-manager-line-status-icon indexer-manager-line-icon-enabled" className="indexer-manager-line-status-icon indexer-manager-line-icon-enabled"
color="#2ecc71" color="#2ecc71"
style={iconStyle} size={24}
/> />
)} )}
</Group> </Group>
@@ -85,11 +96,9 @@ export default function IndexerManagerWidget({ options, integrationIds }: Widget
<Button <Button
className="indexer-manager-test-button" className="indexer-manager-test-button"
w="100%" w="100%"
fz="5cqmin" radius={board.itemRadius}
h="12.5cqmin"
radius="md"
variant="light" variant="light"
leftSection={<IconTestPipe style={iconStyle} />} leftSection={<IconTestPipe size={"1rem"} />}
loading={isPending} loading={isPending}
loaderProps={{ type: "dots" }} loaderProps={{ type: "dots" }}
onClick={() => { onClick={() => {

View File

@@ -4,6 +4,7 @@ import { ActionIcon, Anchor, Avatar, Badge, Card, Group, Image, ScrollArea, Stac
import { IconThumbDown, IconThumbUp } from "@tabler/icons-react"; import { IconThumbDown, IconThumbUp } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
import { useRequiredBoard } from "@homarr/boards/context";
import { MediaAvailability, MediaRequestStatus } from "@homarr/integrations/types"; import { MediaAvailability, MediaRequestStatus } from "@homarr/integrations/types";
import type { ScopedTranslationFunction } from "@homarr/translation"; import type { ScopedTranslationFunction } from "@homarr/translation";
import { useScopedI18n } from "@homarr/translation/client"; import { useScopedI18n } from "@homarr/translation/client";
@@ -54,23 +55,23 @@ export default function MediaServerWidget({
); );
const { mutate: mutateRequestAnswer } = clientApi.widget.mediaRequests.answerRequest.useMutation(); const { mutate: mutateRequestAnswer } = clientApi.widget.mediaRequests.answerRequest.useMutation();
const board = useRequiredBoard();
if (mediaRequests.length === 0) throw new NoIntegrationDataError(); if (mediaRequests.length === 0) throw new NoIntegrationDataError();
return ( return (
<ScrollArea <ScrollArea
className="mediaRequests-list-scrollArea" className="mediaRequests-list-scrollArea"
scrollbarSize="2cqmin" scrollbarSize="md"
style={{ pointerEvents: isEditMode ? "none" : undefined }} style={{ pointerEvents: isEditMode ? "none" : undefined }}
> >
<Stack className="mediaRequests-list-list" gap="2cqmin" p="2cqmin"> <Stack className="mediaRequests-list-list" gap="sm" p="sm">
{mediaRequests.map((mediaRequest) => ( {mediaRequests.map((mediaRequest) => (
<Card <Card
className={`mediaRequests-list-item-wrapper mediaRequests-list-item-${mediaRequest.type} mediaRequests-list-item-${mediaRequest.status}`} className={`mediaRequests-list-item-wrapper mediaRequests-list-item-${mediaRequest.type} mediaRequests-list-item-${mediaRequest.status}`}
key={`${mediaRequest.integrationId}-${mediaRequest.id}`} key={`${mediaRequest.integrationId}-${mediaRequest.id}`}
h="20cqmin" radius={board.itemRadius}
radius="2cqmin" p="sm"
p="2cqmin"
withBorder withBorder
> >
<Image <Image
@@ -93,28 +94,24 @@ export default function MediaServerWidget({
wrap="nowrap" wrap="nowrap"
gap={0} gap={0}
> >
<Group className="mediaRequests-list-item-left-side" h="100%" gap="4cqmin" wrap="nowrap" flex={1}> <Group className="mediaRequests-list-item-left-side" h="100%" gap="md" wrap="nowrap" flex={1}>
<Image <Image
className="mediaRequests-list-item-poster" className="mediaRequests-list-item-poster"
src={mediaRequest.posterImagePath} src={mediaRequest.posterImagePath}
h="100%" h={60}
w="10cqmin" w="auto"
radius="1cqmin" radius={"md"}
/> />
<Stack className="mediaRequests-list-item-media-infos" gap="1cqmin"> <Stack className="mediaRequests-list-item-media-infos" gap={0}>
<Group className="mediaRequests-list-item-info-first-line" gap="2cqmin" wrap="nowrap"> <Group className="mediaRequests-list-item-info-first-line" gap="sm" align={"end"} wrap="nowrap">
<Text className="mediaRequests-list-item-media-year" size="3.5cqmin" pt="0.75cqmin"> <Text className="mediaRequests-list-item-media-year" size="md" pt="xs">
{mediaRequest.airDate?.getFullYear() ?? t("toBeDetermined")} {mediaRequest.airDate?.getFullYear() ?? t("toBeDetermined")}
</Text> </Text>
<Badge <Badge
className="mediaRequests-list-item-media-status" className="mediaRequests-list-item-media-status"
color={getAvailabilityProperties(mediaRequest.availability, t).color} color={getAvailabilityProperties(mediaRequest.availability, t).color}
variant="light" variant="light"
fz="3.5cqmin" size="md"
lh="4cqmin"
size="5cqmin"
pt="0.75cqmin"
px="2cqmin"
> >
{getAvailabilityProperties(mediaRequest.availability, t).label} {getAvailabilityProperties(mediaRequest.availability, t).label}
</Badge> </Badge>
@@ -124,26 +121,27 @@ export default function MediaServerWidget({
href={mediaRequest.href} href={mediaRequest.href}
c="var(--mantine-color-text)" c="var(--mantine-color-text)"
target={options.linksTargetNewTab ? "_blank" : "_self"} target={options.linksTargetNewTab ? "_blank" : "_self"}
fz="5cqmin" fz="md"
fw={"bold"}
lineClamp={1} lineClamp={1}
> >
{mediaRequest.name || "unknown"} {mediaRequest.name || "unknown"}
</Anchor> </Anchor>
</Stack> </Stack>
</Group> </Group>
<Stack className="mediaRequests-list-item-right-side" gap="1cqmin" align="end"> <Stack className="mediaRequests-list-item-right-side" gap="xs" ms={"lg"} align="end">
<Group className="mediaRequests-list-item-request-user" gap="2cqmin" wrap="nowrap"> <Group className="mediaRequests-list-item-request-user" gap="sm" wrap="nowrap">
<Avatar <Avatar
className="mediaRequests-list-item-request-user-avatar" className="mediaRequests-list-item-request-user-avatar"
src={mediaRequest.requestedBy?.avatar} src={mediaRequest.requestedBy?.avatar}
size="6cqmin" size="sm"
/> />
<Anchor <Anchor
className="mediaRequests-list-item-request-user-name" className="mediaRequests-list-item-request-user-name"
href={mediaRequest.requestedBy?.link} href={mediaRequest.requestedBy?.link}
c="var(--mantine-color-text)" c="var(--mantine-color-text)"
target={options.linksTargetNewTab ? "_blank" : "_self"} target={options.linksTargetNewTab ? "_blank" : "_self"}
fz="5cqmin" fz="md"
lineClamp={1} lineClamp={1}
style={{ wordBreak: "break-all" }} style={{ wordBreak: "break-all" }}
> >
@@ -151,13 +149,14 @@ export default function MediaServerWidget({
</Anchor> </Anchor>
</Group> </Group>
{mediaRequest.status === MediaRequestStatus.PendingApproval ? ( {mediaRequest.status === MediaRequestStatus.PendingApproval ? (
<Group className="mediaRequests-list-item-pending-buttons" gap="2cqmin"> <Group className="mediaRequests-list-item-pending-buttons" gap="sm">
<Tooltip label={t("pending.approve")}> <Tooltip label={t("pending.approve")}>
<ActionIcon <ActionIcon
className="mediaRequests-list-item-pending-button-approve" className="mediaRequests-list-item-pending-button-approve"
variant="light" variant="light"
color="green" color="green"
size="5cqmin" size="md"
radius={"md"}
onClick={() => { onClick={() => {
mutateRequestAnswer({ mutateRequestAnswer({
integrationId: mediaRequest.integrationId, integrationId: mediaRequest.integrationId,
@@ -166,7 +165,7 @@ export default function MediaServerWidget({
}); });
}} }}
> >
<IconThumbUp size="4cqmin" /> <IconThumbUp size={23} />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
<Tooltip label={t("pending.decline")}> <Tooltip label={t("pending.decline")}>
@@ -174,7 +173,8 @@ export default function MediaServerWidget({
className="mediaRequests-list-item-pending-button-decline" className="mediaRequests-list-item-pending-button-decline"
variant="light" variant="light"
color="red" color="red"
size="5cqmin" size="md"
radius={"md"}
onClick={() => { onClick={() => {
mutateRequestAnswer({ mutateRequestAnswer({
integrationId: mediaRequest.integrationId, integrationId: mediaRequest.integrationId,
@@ -183,7 +183,7 @@ export default function MediaServerWidget({
}); });
}} }}
> >
<IconThumbDown size="4cqmin" /> <IconThumbDown size={23} />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
</Group> </Group>

View File

@@ -1,7 +1,7 @@
.gridElement:not(:nth-child(8n)) { [data-mantine-color-scheme="light"] .card {
border-right: 0.5cqmin solid var(--app-shell-border-color); background-color: var(--mantine-color-gray-1);
} }
.gridElement:not(:nth-last-child(-n + 8)) { [data-mantine-color-scheme="dark"] .card {
border-bottom: 0.5cqmin solid var(--app-shell-border-color); background-color: var(--mantine-color-dark-7);
} }

View File

@@ -17,6 +17,7 @@ import {
import combineClasses from "clsx"; import combineClasses from "clsx";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
import { useRequiredBoard } from "@homarr/boards/context";
import type { RequestStats } from "@homarr/integrations/types"; import type { RequestStats } from "@homarr/integrations/types";
import { useScopedI18n } from "@homarr/translation/client"; import { useScopedI18n } from "@homarr/translation/client";
@@ -42,9 +43,10 @@ export default function MediaServerWidget({
const { width, height, ref } = useElementSize(); const { width, height, ref } = useElementSize();
const board = useRequiredBoard();
if (requestStats.users.length === 0 && requestStats.stats.length === 0) throw new NoIntegrationDataError(); if (requestStats.users.length === 0 && requestStats.stats.length === 0) throw new NoIntegrationDataError();
//Add processing and available
const data = [ const data = [
{ {
name: "approved", name: "approved",
@@ -93,37 +95,35 @@ export default function MediaServerWidget({
className="mediaRequests-stats-layout" className="mediaRequests-stats-layout"
display="flex" display="flex"
h="100%" h="100%"
gap="2cqmin" gap="sm"
p="2cqmin" p="sm"
align="center" align="center"
style={{ pointerEvents: isEditMode ? "none" : undefined }} style={{ pointerEvents: isEditMode ? "none" : undefined }}
> >
<Text className="mediaRequests-stats-stats-title" size="6.5cqmin"> <Text className="mediaRequests-stats-stats-title" fw={"bold"} size="md">
{t("titles.stats.main")} {t("titles.stats.main")}
</Text> </Text>
<Grid className="mediaRequests-stats-stats-grid" gutter={0} w="100%"> <Grid className="mediaRequests-stats-stats-grid" gutter={10} w="100%">
{data.map((stat) => ( {data.map((stat) => (
<Grid.Col <Grid.Col
className={combineClasses( className={combineClasses("mediaRequests-stats-stat-wrapper", `mediaRequests-stats-stat-${stat.name}`)}
classes.gridElement,
"mediaRequests-stats-stat-wrapper",
`mediaRequests-stats-stat-${stat.name}`,
)}
key={stat.name} key={stat.name}
span={3} span={3}
> >
<Tooltip label={t(`titles.stats.${stat.name}`)}> <Tooltip label={t(`titles.stats.${stat.name}`)}>
<Stack className="mediaRequests-stats-stat-stack" align="center" gap="2cqmin" p="2cqmin"> <Card p={0} radius={board.itemRadius} className={classes.card}>
<stat.icon className="mediaRequests-stats-stat-icon" size="7.5cqmin" /> <Stack className="mediaRequests-stats-stat-stack" align="center" gap={0} p="xs">
<Text className="mediaRequests-stats-stat-value" size="5cqmin"> <stat.icon className="mediaRequests-stats-stat-icon" size={30} />
{stat.number} <Text className="mediaRequests-stats-stat-value" size="md">
</Text> {stat.number}
</Stack> </Text>
</Stack>
</Card>
</Tooltip> </Tooltip>
</Grid.Col> </Grid.Col>
))} ))}
</Grid> </Grid>
<Text className="mediaRequests-stats-users-title" size="6.5cqmin"> <Text className="mediaRequests-stats-users-title" fw={"bold"} size="md">
{t("titles.users.main")} {t("titles.users.main")}
</Text> </Text>
<Stack <Stack
@@ -132,36 +132,34 @@ export default function MediaServerWidget({
w="100%" w="100%"
ref={ref} ref={ref}
display="flex" display="flex"
gap="2cqmin" gap="sm"
style={{ overflow: "hidden" }} style={{ overflow: "hidden", justifyContent: "end" }}
> >
{requestStats.users.slice(0, Math.max(Math.floor((height / width) * 5), 1)).map((user) => ( {requestStats.users.slice(0, Math.max(Math.floor((height / width) * 5), 1)).map((user) => (
<Card <Card
className={combineClasses( className={combineClasses(
"mediaRequests-stats-users-user-wrapper", "mediaRequests-stats-users-user-wrapper",
`mediaRequests-stats-users-user-${user.id}`, `mediaRequests-stats-users-user-${user.id}`,
classes.card,
)} )}
key={user.id} key={user.id}
withBorder p="sm"
p="2cqmin" radius={board.itemRadius}
flex={1}
mah="38.5cqmin"
radius="2.5cqmin"
> >
<Group className="mediaRequests-stats-users-user-group" h="100%" p={0} gap="2cqmin" display="flex"> <Group className="mediaRequests-stats-users-user-group" h="100%" p={0} gap="sm" display="flex">
<Tooltip label={user.integration.name}> <Tooltip label={user.integration.name}>
<Avatar <Avatar
className="mediaRequests-stats-users-user-avatar" className="mediaRequests-stats-users-user-avatar"
size="12.5cqmin" size="md"
src={user.avatar} src={user.avatar}
bd={`0.5cqmin solid ${user.integration.kind === "overseerr" ? "#ECB000" : "#6677CC"}`} bd={`2px solid ${user.integration.kind === "overseerr" ? "#ECB000" : "#6677CC"}`}
/> />
</Tooltip> </Tooltip>
<Stack className="mediaRequests-stats-users-user-infos" gap="2cqmin"> <Stack className="mediaRequests-stats-users-user-infos" gap={0}>
<Text className="mediaRequests-stats-users-user-userName" size="6cqmin"> <Text className="mediaRequests-stats-users-user-userName" fw={"bold"} size="md">
{user.displayName} {user.displayName}
</Text> </Text>
<Text className="mediaRequests-stats-users-user-request-count" size="4cqmin"> <Text className="mediaRequests-stats-users-user-request-count" size="md">
{`${t("titles.users.requests")}: ${user.requestCount}`} {`${t("titles.users.requests")}: ${user.requestCount}`}
</Text> </Text>
</Stack> </Stack>
@@ -170,11 +168,12 @@ export default function MediaServerWidget({
className="mediaRequests-stats-users-user-link-button" className="mediaRequests-stats-users-user-link-button"
variant="light" variant="light"
color="var(--mantine-color-text)" color="var(--mantine-color-text)"
size="10cqmin" size={40}
component="a" component="a"
radius={board.itemRadius}
href={user.link} href={user.link}
> >
<IconExternalLink className="mediaRequests-stats-users-user-link-icon" size="7.5cqmin" /> <IconExternalLink className="mediaRequests-stats-users-user-link-icon" size={25} />
</ActionIcon> </ActionIcon>
</Group> </Group>
</Card> </Card>

View File

@@ -1,7 +1,6 @@
"use client"; "use client";
import { useMemo } from "react"; import { useMemo } from "react";
import type { MantineStyleProp } from "@mantine/core";
import { Avatar, Box, Flex, Group, Stack, Text, Title } from "@mantine/core"; import { Avatar, Box, Flex, Group, Stack, Text, Title } from "@mantine/core";
import { IconDeviceAudioTape, IconDeviceTv, IconMovie, IconVideo } from "@tabler/icons-react"; import { IconDeviceAudioTape, IconDeviceTv, IconMovie, IconVideo } from "@tabler/icons-react";
import type { MRT_ColumnDef } from "mantine-react-table"; import type { MRT_ColumnDef } from "mantine-react-table";
@@ -36,13 +35,11 @@ export default function MediaServerWidget({ integrationIds, isEditMode }: Widget
header: "Name", header: "Name",
mantineTableHeadCellProps: { mantineTableHeadCellProps: {
style: { style: {
fontSize: "7cqmin",
padding: "2cqmin",
width: "30%", width: "30%",
}, },
}, },
Cell: ({ row }) => ( Cell: ({ row }) => (
<Text size="7cqmin" style={{ overflow: "hidden", textOverflow: "ellipsis" }}> <Text size="md" style={{ overflow: "hidden", textOverflow: "ellipsis" }}>
{row.original.sessionName} {row.original.sessionName}
</Text> </Text>
), ),
@@ -52,15 +49,13 @@ export default function MediaServerWidget({ integrationIds, isEditMode }: Widget
header: "User", header: "User",
mantineTableHeadCellProps: { mantineTableHeadCellProps: {
style: { style: {
fontSize: "7cqmin",
padding: "2cqmin",
width: "25%", width: "25%",
}, },
}, },
Cell: ({ row }) => ( Cell: ({ row }) => (
<Group gap={"2cqmin"}> <Group gap={"sm"}>
<Avatar src={row.original.user.profilePictureUrl} size={"10cqmin"} /> <Avatar src={row.original.user.profilePictureUrl} size={30} />
<Text size="7cqmin">{row.original.user.username}</Text> <Text size="md">{row.original.user.username}</Text>
</Group> </Group>
), ),
}, },
@@ -69,8 +64,6 @@ export default function MediaServerWidget({ integrationIds, isEditMode }: Widget
header: "Currently playing", header: "Currently playing",
mantineTableHeadCellProps: { mantineTableHeadCellProps: {
style: { style: {
fontSize: "7cqmin",
padding: "2cqmin",
width: "45%", width: "45%",
}, },
}, },
@@ -78,9 +71,7 @@ export default function MediaServerWidget({ integrationIds, isEditMode }: Widget
if (row.original.currentlyPlaying) { if (row.original.currentlyPlaying) {
return ( return (
<Box> <Box>
<Text size="7cqmin" style={{ whiteSpace: "normal" }}> <Text lineClamp={1}>{row.original.currentlyPlaying.name}</Text>
{row.original.currentlyPlaying.name}
</Text>
</Box> </Box>
); );
} }
@@ -129,14 +120,6 @@ export default function MediaServerWidget({ integrationIds, isEditMode }: Widget
[currentStreams], [currentStreams],
); );
const baseStyle: MantineStyleProp = {
"--total-width": "calc(100cqw / var(--total-width))",
"--ratio-width": "calc(100cqw / var(--total-width))",
"--space-size": "calc(var(--ratio-width) * 0.1)", //Standard gap and spacing value
"--text-fz": "calc(var(--ratio-width) * 0.45)", //General Font Size
"--icon-size": "calc(var(--ratio-width) * 2 / 3)", //Normal icon size
"--mrt-base-background-color": "transparent",
};
const { openModal } = useModalAction(itemInfoModal); const { openModal } = useModalAction(itemInfoModal);
const table = useTranslatedMantineReactTable({ const table = useTranslatedMantineReactTable({
columns, columns,
@@ -202,21 +185,21 @@ export default function MediaServerWidget({ integrationIds, isEditMode }: Widget
}); });
return ( return (
<Stack gap={0} h="100%" display="flex" style={baseStyle}> <Stack gap={0} h="100%" display="flex">
<MantineReactTable table={table} /> <MantineReactTable table={table} />
<Group <Group
gap="1cqmin" gap="xs"
h="var(--ratio-width)" h={30}
px="var(--space-size)" px="xs"
pr="5cqmin" pr="md"
justify="flex-end" justify="flex-end"
style={{ style={{
borderTop: "0.0625rem solid var(--border-color)", borderTop: "1px solid var(--border-color)",
}} }}
> >
{uniqueIntegrations.map((integration) => ( {uniqueIntegrations.map((integration) => (
<Group key={integration.integrationKind} gap="1cqmin" align="center"> <Group key={integration.integrationKind} gap="xs" align="center">
<Avatar className="media-server-icon" src={integration.integrationIcon} size="xs" /> <Avatar className="media-server-icon" src={integration.integrationIcon} radius={"xs"} size="xs" />
<Text className="media-server-name" size="sm"> <Text className="media-server-name" size="sm">
{integration.integrationName} {integration.integrationName}
</Text> </Text>

View File

@@ -4,6 +4,7 @@ import { Box, Flex, Group, Text, Tooltip } from "@mantine/core";
import { IconUsersGroup } from "@tabler/icons-react"; import { IconUsersGroup } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
import { formatNumber } from "@homarr/common";
import { useScopedI18n } from "@homarr/translation/client"; import { useScopedI18n } from "@homarr/translation/client";
import type { WidgetComponentProps } from "../../definition"; import type { WidgetComponentProps } from "../../definition";
@@ -29,15 +30,15 @@ export default function MinecraftServerStatusWidget({ options }: WidgetComponent
h="100%" h="100%"
w="100%" w="100%"
direction="column" direction="column"
p="7.5cqmin" p="sm"
justify="center" justify="center"
align="center" align="center"
> >
<Group gap="5cqmin" wrap="nowrap" align="center"> <Group gap="xs" wrap="nowrap" align="center">
<Tooltip label={data.online ? tStatus("online") : tStatus("offline")}> <Tooltip label={data.online ? tStatus("online") : tStatus("offline")}>
<Box w="8cqmin" h="8cqmin" bg={data.online ? "teal" : "red"} style={{ borderRadius: "100%" }}></Box> <Box w="md" h="md" bg={data.online ? "teal" : "red"} style={{ borderRadius: "100%" }}></Box>
</Tooltip> </Tooltip>
<Text size="10cqmin" fw="bold"> <Text size="md" fw="bold">
{title} {title}
</Text> </Text>
</Group> </Group>
@@ -48,10 +49,10 @@ export default function MinecraftServerStatusWidget({ options }: WidgetComponent
alt={`minecraft icon ${options.domain}`} alt={`minecraft icon ${options.domain}`}
src={data.icon} src={data.icon}
/> />
<Group gap="2cqmin" c="gray.6" align="center"> <Group gap={5} c="gray.6" align="center">
<IconUsersGroup style={{ width: "10cqmin", height: "10cqmin" }} /> <IconUsersGroup size="1rem" />
<Text size="10cqmin"> <Text size="md">
{data.players.online}/{data.players.max} {formatNumber(data.players.online, 1)} / {formatNumber(data.players.max, 1)}
</Text> </Text>
</Group> </Group>
</> </>

View File

@@ -66,12 +66,12 @@ import type { TablerIcon } from "@homarr/ui";
import type { WidgetComponentProps } from "../definition"; import type { WidgetComponentProps } from "../definition";
const iconProps = { const iconProps = {
size: "5cqmin", size: 30,
stroke: 1.5, stroke: 1.5,
}; };
const controlIconProps = { const controlIconProps = {
size: "5cqmin", size: 20,
stroke: 1.5, stroke: 1.5,
}; };

View File

@@ -5,6 +5,7 @@ import { IconClock } from "@tabler/icons-react";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
import { useRequiredBoard } from "@homarr/boards/context";
import type { WidgetComponentProps } from "../definition"; import type { WidgetComponentProps } from "../definition";
import classes from "./component.module.scss"; import classes from "./component.module.scss";
@@ -22,29 +23,30 @@ export default function RssFeed({ options }: WidgetComponentProps<"rssFeed">) {
retry: false, retry: false,
}, },
); );
const board = useRequiredBoard();
const languageDir = options.enableRtl ? "RTL" : "LTR"; const languageDir = options.enableRtl ? "RTL" : "LTR";
return ( return (
<ScrollArea className="scroll-area-w100" w="100%" p="4cqmin"> <ScrollArea className="scroll-area-w100" w="100%" p="sm">
<Stack w={"100%"} gap="4cqmin"> <Stack w={"100%"} gap="sm">
{feedEntries.map((feedEntry) => ( {feedEntries.map((feedEntry) => (
<Card <Card
key={feedEntry.id} key={feedEntry.id}
withBorder withBorder
component={"a"} component={"a"}
href={feedEntry.link} href={feedEntry.link}
radius="2.5cqmin" radius={board.itemRadius}
target="_blank" target="_blank"
w="100%" w="100%"
p="2.5cqmin" p="sm"
> >
{feedEntry.enclosure && ( {feedEntry.enclosure && (
<Image className={classes.backgroundImage} src={feedEntry.enclosure} alt="backdrop" /> <Image className={classes.backgroundImage} src={feedEntry.enclosure} alt="backdrop" />
)} )}
<Flex gap="2.5cqmin" direction="column" w="100%"> <Flex gap="sm" direction="column" w="100%">
<Text dir={languageDir} fz="4cqmin" lh="5cqmin" lineClamp={2}> <Text dir={languageDir} fz="sm" lh="sm" lineClamp={2}>
{feedEntry.title} {feedEntry.title}
</Text> </Text>
{feedEntry.description && ( {feedEntry.description && (
@@ -52,7 +54,7 @@ export default function RssFeed({ options }: WidgetComponentProps<"rssFeed">) {
className={feedEntry.description} className={feedEntry.description}
dir={languageDir} dir={languageDir}
c="dimmed" c="dimmed"
size="3.5cqmin" size="sm"
lineClamp={options.textLinesClamp as number} lineClamp={options.textLinesClamp as number}
dangerouslySetInnerHTML={{ __html: feedEntry.description }} dangerouslySetInnerHTML={{ __html: feedEntry.description }}
/> />
@@ -68,9 +70,9 @@ export default function RssFeed({ options }: WidgetComponentProps<"rssFeed">) {
} }
const InfoDisplay = ({ date }: { date: string }) => ( const InfoDisplay = ({ date }: { date: string }) => (
<Group gap="2.5cqmin"> <Group gap={5} align={"center"}>
<IconClock size="2.5cqmin" /> <IconClock size={"1rem"} color={"var(--mantine-color-dimmed)"} />
<Text size="2.5cqmin" c="dimmed" pt="1cqmin"> <Text size="sm" c="dimmed">
{date} {date}
</Text> </Text>
</Group> </Group>

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { Box, Group, HoverCard, Space, Stack, Text } from "@mantine/core"; import { Box, Group, HoverCard, Stack, Text } from "@mantine/core";
import { IconArrowDownRight, IconArrowUpRight, IconMapPin, IconWind } from "@tabler/icons-react"; import { IconArrowDownRight, IconArrowUpRight, IconMapPin, IconWind } from "@tabler/icons-react";
import combineClasses from "clsx"; import combineClasses from "clsx";
import dayjs from "dayjs"; import dayjs from "dayjs";
@@ -28,8 +28,8 @@ export default function WeatherWidget({ isEditMode, options }: WidgetComponentPr
return ( return (
<Stack <Stack
align="center" align="center"
gap="sm"
justify="center" justify="center"
gap="0"
w="100%" w="100%"
h="100%" h="100%"
style={{ pointerEvents: isEditMode ? "none" : undefined }} style={{ pointerEvents: isEditMode ? "none" : undefined }}
@@ -49,20 +49,21 @@ interface WeatherProps extends Pick<WidgetComponentProps<"weather">, "options">
const DailyWeather = ({ options, weather }: WeatherProps) => { const DailyWeather = ({ options, weather }: WeatherProps) => {
const t = useScopedI18n("widget.weather"); const t = useScopedI18n("widget.weather");
return ( return (
<> <>
<Group className="weather-day-group" gap="1cqmin"> <Group className="weather-day-group" gap="sm">
<HoverCard> <HoverCard>
<HoverCard.Target> <HoverCard.Target>
<Box> <Box>
<WeatherIcon size="20cqmin" code={weather.current.weathercode} /> <WeatherIcon size={30} code={weather.current.weathercode} />
</Box> </Box>
</HoverCard.Target> </HoverCard.Target>
<HoverCard.Dropdown> <HoverCard.Dropdown>
<WeatherDescription weatherOnly weatherCode={weather.current.weathercode} /> <WeatherDescription weatherOnly weatherCode={weather.current.weathercode} />
</HoverCard.Dropdown> </HoverCard.Dropdown>
</HoverCard> </HoverCard>
<Text fz="17.5cqmin"> <Text fz={30}>
{getPreferredUnit( {getPreferredUnit(
weather.current.temperature, weather.current.temperature,
options.isFormatFahrenheit, options.isFormatFahrenheit,
@@ -70,31 +71,41 @@ const DailyWeather = ({ options, weather }: WeatherProps) => {
)} )}
</Text> </Text>
</Group> </Group>
<Space h="1cqmin" /> <Stack gap="xs" align="center">
{options.showCurrentWindSpeed && ( {options.showCurrentWindSpeed && (
<Group className="weather-current-wind-speed-group" wrap="nowrap" gap="1cqmin"> <Group className="weather-current-wind-speed-group" wrap="nowrap" gap="xs">
<IconWind size="12.5cqmin" /> <IconWind size={16} />
<Text fz="10cqmin">{t("currentWindSpeed", { currentWindSpeed: weather.current.windspeed })}</Text> <Text fz={16}>{t("currentWindSpeed", { currentWindSpeed: weather.current.windspeed })}</Text>
</Group>
)}
<Group className="weather-max-min-temp-group" wrap="nowrap" gap="sm">
<Group gap="xs" wrap="nowrap">
<IconArrowUpRight size={16} />
<Text fz={16}>
{getPreferredUnit(
weather.daily[0]?.maxTemp,
options.isFormatFahrenheit,
options.disableTemperatureDecimals,
)}
</Text>
</Group>
<Group gap="xs" wrap="nowrap">
<IconArrowDownRight size={16} />
<Text fz={16}>
{getPreferredUnit(
weather.daily[0]?.minTemp,
options.isFormatFahrenheit,
options.disableTemperatureDecimals,
)}
</Text>
</Group>
</Group> </Group>
)} </Stack>
<Space h="1cqmin" />
<Group className="weather-max-min-temp-group" wrap="nowrap" gap="1cqmin">
<IconArrowUpRight size="12.5cqmin" />
<Text fz="10cqmin">
{getPreferredUnit(weather.daily[0]?.maxTemp, options.isFormatFahrenheit, options.disableTemperatureDecimals)}
</Text>
<Space w="2.5cqmin" />
<IconArrowDownRight size="12.5cqmin" />
<Text fz="10cqmin">
{getPreferredUnit(weather.daily[0]?.minTemp, options.isFormatFahrenheit, options.disableTemperatureDecimals)}
</Text>
</Group>
{options.showCity && ( {options.showCity && (
<> <>
<Space h="5cqmin" /> <Group className="weather-city-group" wrap="nowrap" gap="xs">
<Group className="weather-city-group" wrap="nowrap" gap="1cqmin"> <IconMapPin size={16} />
<IconMapPin size="12.5cqmin" /> <Text fz={16} style={{ whiteSpace: "nowrap" }}>
<Text size="12.5cqmin" style={{ whiteSpace: "nowrap" }}>
{options.location.name} {options.location.name}
</Text> </Text>
</Group> </Group>
@@ -107,35 +118,35 @@ const DailyWeather = ({ options, weather }: WeatherProps) => {
const WeeklyForecast = ({ options, weather }: WeatherProps) => { const WeeklyForecast = ({ options, weather }: WeatherProps) => {
return ( return (
<> <>
<Group className="weather-forecast-city-temp-group" wrap="nowrap" gap="5cqmin"> <Group className="weather-forecast-city-temp-group" wrap="nowrap" gap="md">
{options.showCity && ( {options.showCity && (
<> <Group gap="xs" wrap="nowrap">
<IconMapPin size="20cqmin" /> <IconMapPin size={16} />
<Text size="15cqmin" style={{ whiteSpace: "nowrap" }}> <Text fz={16} style={{ whiteSpace: "nowrap" }}>
{options.location.name} {options.location.name}
</Text> </Text>
<Space w="20cqmin" /> </Group>
</>
)} )}
<HoverCard> <Group gap="xs" wrap="nowrap">
<HoverCard.Target> <HoverCard>
<Box> <HoverCard.Target>
<WeatherIcon size="20cqmin" code={weather.current.weathercode} /> <Box>
</Box> <WeatherIcon size={16} code={weather.current.weathercode} />
</HoverCard.Target> </Box>
<HoverCard.Dropdown> </HoverCard.Target>
<WeatherDescription weatherOnly weatherCode={weather.current.weathercode} /> <HoverCard.Dropdown>
</HoverCard.Dropdown> <WeatherDescription weatherOnly weatherCode={weather.current.weathercode} />
</HoverCard> </HoverCard.Dropdown>
<Text fz="20cqmin"> </HoverCard>
{getPreferredUnit( <Text fz={16}>
weather.current.temperature, {getPreferredUnit(
options.isFormatFahrenheit, weather.current.temperature,
options.disableTemperatureDecimals, options.isFormatFahrenheit,
)} options.disableTemperatureDecimals,
</Text> )}
</Text>
</Group>
</Group> </Group>
<Space h="2.5cqmin" />
<Forecast weather={weather} options={options} /> <Forecast weather={weather} options={options} />
</> </>
); );
@@ -144,7 +155,7 @@ const WeeklyForecast = ({ options, weather }: WeatherProps) => {
function Forecast({ weather, options }: WeatherProps) { function Forecast({ weather, options }: WeatherProps) {
const dateFormat = options.dateFormat; const dateFormat = options.dateFormat;
return ( return (
<Group className="weather-forecast-days-group" w="100%" justify="space-evenly" wrap="nowrap" pb="2.5cqmin"> <Group className="weather-forecast-days-group" w="100%" justify="space-evenly" wrap="nowrap" pb="sm">
{weather.daily.slice(0, options.forecastDayCount).map((dayWeather, index) => ( {weather.daily.slice(0, options.forecastDayCount).map((dayWeather, index) => (
<HoverCard key={dayWeather.time} withArrow shadow="md"> <HoverCard key={dayWeather.time} withArrow shadow="md">
<HoverCard.Target> <HoverCard.Target>
@@ -157,9 +168,9 @@ function Forecast({ weather, options }: WeatherProps) {
gap="0" gap="0"
align="center" align="center"
> >
<Text fz="10cqmin">{dayjs(dayWeather.time).format("dd")}</Text> <Text fz="xl">{dayjs(dayWeather.time).format("dd")}</Text>
<WeatherIcon size="15cqmin" code={dayWeather.weatherCode} /> <WeatherIcon size={16} code={dayWeather.weatherCode} />
<Text fz="10cqmin"> <Text fz={16}>
{getPreferredUnit(dayWeather.maxTemp, options.isFormatFahrenheit, options.disableTemperatureDecimals)} {getPreferredUnit(dayWeather.maxTemp, options.isFormatFahrenheit, options.disableTemperatureDecimals)}
</Text> </Text>
</Stack> </Stack>

247
pnpm-lock.yaml generated
View File

@@ -44,11 +44,11 @@ importers:
specifier: ^4.3.4 specifier: ^4.3.4
version: 4.3.4(vite@5.4.5(@types/node@22.13.9)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0)) version: 4.3.4(vite@5.4.5(@types/node@22.13.9)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0))
'@vitest/coverage-v8': '@vitest/coverage-v8':
specifier: ^3.0.7 specifier: ^3.0.8
version: 3.0.7(vitest@3.0.7) version: 3.0.8(vitest@3.0.8)
'@vitest/ui': '@vitest/ui':
specifier: ^3.0.7 specifier: ^3.0.8
version: 3.0.7(vitest@3.0.7) version: 3.0.8(vitest@3.0.8)
conventional-changelog-conventionalcommits: conventional-changelog-conventionalcommits:
specifier: ^8.0.0 specifier: ^8.0.0
version: 8.0.0 version: 8.0.0
@@ -77,8 +77,8 @@ importers:
specifier: ^5.1.4 specifier: ^5.1.4
version: 5.1.4(typescript@5.8.2)(vite@5.4.5(@types/node@22.13.9)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0)) version: 5.1.4(typescript@5.8.2)(vite@5.4.5(@types/node@22.13.9)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0))
vitest: vitest:
specifier: ^3.0.7 specifier: ^3.0.8
version: 3.0.7(@types/node@22.13.9)(@vitest/ui@3.0.7)(jsdom@26.0.0)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0) version: 3.0.8(@types/node@22.13.9)(@vitest/ui@3.0.8)(jsdom@26.0.0)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0)
apps/nextjs: apps/nextjs:
dependencies: dependencies:
@@ -206,23 +206,23 @@ importers:
specifier: ^3.31.0 specifier: ^3.31.0
version: 3.31.0(react@19.0.0) version: 3.31.0(react@19.0.0)
'@tanstack/react-query': '@tanstack/react-query':
specifier: ^5.67.1 specifier: ^5.67.2
version: 5.67.1(react@19.0.0) version: 5.67.2(react@19.0.0)
'@tanstack/react-query-devtools': '@tanstack/react-query-devtools':
specifier: ^5.67.1 specifier: ^5.67.2
version: 5.67.1(@tanstack/react-query@5.67.1(react@19.0.0))(react@19.0.0) version: 5.67.2(@tanstack/react-query@5.67.2(react@19.0.0))(react@19.0.0)
'@tanstack/react-query-next-experimental': '@tanstack/react-query-next-experimental':
specifier: ^5.67.1 specifier: ^5.67.2
version: 5.67.1(@tanstack/react-query@5.67.1(react@19.0.0))(next@15.1.7(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.85.1))(react@19.0.0) version: 5.67.2(@tanstack/react-query@5.67.2(react@19.0.0))(next@15.1.7(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.85.1))(react@19.0.0)
'@trpc/client': '@trpc/client':
specifier: next specifier: next
version: 11.0.0-rc.824(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(typescript@5.8.2) version: 11.0.0-rc.824(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(typescript@5.8.2)
'@trpc/next': '@trpc/next':
specifier: next specifier: next
version: 11.0.0-rc.824(@tanstack/react-query@5.67.1(react@19.0.0))(@trpc/client@11.0.0-rc.824(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(typescript@5.8.2))(@trpc/react-query@11.0.0-rc.824(@tanstack/react-query@5.67.1(react@19.0.0))(@trpc/client@11.0.0-rc.824(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(typescript@5.8.2))(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.8.2))(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(next@15.1.7(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.85.1))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.8.2) version: 11.0.0-rc.824(@tanstack/react-query@5.67.2(react@19.0.0))(@trpc/client@11.0.0-rc.824(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(typescript@5.8.2))(@trpc/react-query@11.0.0-rc.824(@tanstack/react-query@5.67.2(react@19.0.0))(@trpc/client@11.0.0-rc.824(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(typescript@5.8.2))(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.8.2))(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(next@15.1.7(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.85.1))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.8.2)
'@trpc/react-query': '@trpc/react-query':
specifier: next specifier: next
version: 11.0.0-rc.824(@tanstack/react-query@5.67.1(react@19.0.0))(@trpc/client@11.0.0-rc.824(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(typescript@5.8.2))(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.8.2) version: 11.0.0-rc.824(@tanstack/react-query@5.67.2(react@19.0.0))(@trpc/client@11.0.0-rc.824(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(typescript@5.8.2))(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.8.2)
'@trpc/server': '@trpc/server':
specifier: next specifier: next
version: 11.0.0-rc.824(typescript@5.8.2) version: 11.0.0-rc.824(typescript@5.8.2)
@@ -583,7 +583,7 @@ importers:
version: 11.0.0-rc.824(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(typescript@5.8.2) version: 11.0.0-rc.824(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(typescript@5.8.2)
'@trpc/react-query': '@trpc/react-query':
specifier: next specifier: next
version: 11.0.0-rc.824(@tanstack/react-query@5.67.1(react@19.0.0))(@trpc/client@11.0.0-rc.824(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(typescript@5.8.2))(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.8.2) version: 11.0.0-rc.824(@tanstack/react-query@5.67.2(react@19.0.0))(@trpc/client@11.0.0-rc.824(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(typescript@5.8.2))(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.8.2)
'@trpc/server': '@trpc/server':
specifier: next specifier: next
version: 11.0.0-rc.824(typescript@5.8.2) version: 11.0.0-rc.824(typescript@5.8.2)
@@ -1031,13 +1031,13 @@ importers:
version: 0.30.5 version: 0.30.5
drizzle-orm: drizzle-orm:
specifier: ^0.40.0 specifier: ^0.40.0
version: 0.40.0(@libsql/client-wasm@0.14.0)(@types/better-sqlite3@7.6.12)(better-sqlite3@11.8.1)(gel@2.0.0)(mysql2@3.12.0) version: 0.40.0(@libsql/client-wasm@0.14.0)(@types/better-sqlite3@7.6.12)(better-sqlite3@11.8.1)(gel@2.0.0)(mysql2@3.13.0)
drizzle-zod: drizzle-zod:
specifier: ^0.7.0 specifier: ^0.7.0
version: 0.7.0(drizzle-orm@0.40.0(@libsql/client-wasm@0.14.0)(@types/better-sqlite3@7.6.12)(better-sqlite3@11.8.1)(gel@2.0.0)(mysql2@3.12.0))(zod@3.24.2) version: 0.7.0(drizzle-orm@0.40.0(@libsql/client-wasm@0.14.0)(@types/better-sqlite3@7.6.12)(better-sqlite3@11.8.1)(gel@2.0.0)(mysql2@3.13.0))(zod@3.24.2)
mysql2: mysql2:
specifier: 3.12.0 specifier: 3.13.0
version: 3.12.0 version: 3.13.0
devDependencies: devDependencies:
'@homarr/eslint-config': '@homarr/eslint-config':
specifier: workspace:^0.2.0 specifier: workspace:^0.2.0
@@ -1332,8 +1332,8 @@ importers:
specifier: workspace:^0.1.0 specifier: workspace:^0.1.0
version: link:../env version: link:../env
ioredis: ioredis:
specifier: 5.5.0 specifier: 5.6.0
version: 5.5.0 version: 5.6.0
superjson: superjson:
specifier: 2.2.2 specifier: 2.2.2
version: 2.2.2 version: 2.2.2
@@ -1648,8 +1648,8 @@ importers:
specifier: workspace:^ specifier: workspace:^
version: link:../log version: link:../log
ioredis: ioredis:
specifier: 5.5.0 specifier: 5.6.0
version: 5.5.0 version: 5.6.0
superjson: superjson:
specifier: 2.2.2 specifier: 2.2.2
version: 2.2.2 version: 2.2.2
@@ -2167,8 +2167,8 @@ importers:
specifier: 15.1.7 specifier: 15.1.7
version: 15.1.7 version: 15.1.7
eslint-config-prettier: eslint-config-prettier:
specifier: ^10.0.2 specifier: ^10.1.1
version: 10.0.2(eslint@9.21.0) version: 10.1.1(eslint@9.21.0)
eslint-config-turbo: eslint-config-turbo:
specifier: ^2.4.4 specifier: ^2.4.4
version: 2.4.4(eslint@9.21.0)(turbo@2.4.4) version: 2.4.4(eslint@9.21.0)(turbo@2.4.4)
@@ -4256,27 +4256,27 @@ packages:
resolution: {integrity: sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==} resolution: {integrity: sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==}
engines: {node: '>=12'} engines: {node: '>=12'}
'@tanstack/query-core@5.67.1': '@tanstack/query-core@5.67.2':
resolution: {integrity: sha512-AkFmuukVejyqVIjEQoFhLb3q+xHl7JG8G9cANWTMe3s8iKzD9j1VBSYXgCjy6vm6xM8cUCR9zP2yqWxY9pTWOA==} resolution: {integrity: sha512-+iaFJ/pt8TaApCk6LuZ0WHS/ECVfTzrxDOEL9HH9Dayyb5OVuomLzDXeSaI2GlGT/8HN7bDGiRXDts3LV+u6ww==}
'@tanstack/query-devtools@5.65.0': '@tanstack/query-devtools@5.67.2':
resolution: {integrity: sha512-g5y7zc07U9D3esMdqUfTEVu9kMHoIaVBsD0+M3LPdAdD710RpTcLiNvJY1JkYXqkq9+NV+CQoemVNpQPBXVsJg==} resolution: {integrity: sha512-O4QXFFd7xqp6EX7sdvc9tsVO8nm4lpWBqwpgjpVLW5g7IeOY6VnS/xvs/YzbRhBVkKTMaJMOUGU7NhSX+YGoNg==}
'@tanstack/react-query-devtools@5.67.1': '@tanstack/react-query-devtools@5.67.2':
resolution: {integrity: sha512-a/2I8ORNalh+ek6Nyb9mEiq2u7vydjVMvaQz5ZieGq7r7DxgIFcPiMs4Ay0qkQvHfptESgXR5nImGTHmmt19yQ==} resolution: {integrity: sha512-cmj2DxBc+/9btQ66n5xI8wTtAma2BLVa403K7zIYiguzJ/kV201jnGensYqJeu1Rd8uRMLLRM74jLVMLDWNRJA==}
peerDependencies: peerDependencies:
'@tanstack/react-query': ^5.67.1 '@tanstack/react-query': ^5.67.2
react: ^18 || ^19 react: ^18 || ^19
'@tanstack/react-query-next-experimental@5.67.1': '@tanstack/react-query-next-experimental@5.67.2':
resolution: {integrity: sha512-gNkIksA/jy5lLuAEzkn4aLLmGb1tPlJdbAgnm6GF29uVI4on6D0LfefenYz9Qv/Y8OfACx7XAjpRwisPJBooVQ==} resolution: {integrity: sha512-KTYjhfx1BblTMOKQhmzdSMCkJdl8twtuDqaNeKR1KwU8qqsypXyk5lqfZ3bX5XtGOfAfrtINrVNACmE7vopDdw==}
peerDependencies: peerDependencies:
'@tanstack/react-query': ^5.67.1 '@tanstack/react-query': ^5.67.2
next: ^13 || ^14 || ^15 next: ^13 || ^14 || ^15
react: ^18 || ^19 react: ^18 || ^19
'@tanstack/react-query@5.67.1': '@tanstack/react-query@5.67.2':
resolution: {integrity: sha512-fH5u4JLwB6A+wLFdi8wWBWAYoJV5deYif2OveJ26ktAWjU499uvVFS1wPWnyEyq5LvZX1MZInvv9QRaIZANRaQ==} resolution: {integrity: sha512-6Sa+BVNJWhAV4QHvIqM73norNeGRWGC3ftN0Ix87cmMvI215I1wyJ44KUTt/9a0V9YimfGcg25AITaYVel71Og==}
peerDependencies: peerDependencies:
react: ^18 || ^19 react: ^18 || ^19
@@ -4831,20 +4831,20 @@ packages:
peerDependencies: peerDependencies:
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 vite: ^4.2.0 || ^5.0.0 || ^6.0.0
'@vitest/coverage-v8@3.0.7': '@vitest/coverage-v8@3.0.8':
resolution: {integrity: sha512-Av8WgBJLTrfLOer0uy3CxjlVuWK4CzcLBndW1Nm2vI+3hZ2ozHututkfc7Blu1u6waeQ7J8gzPK/AsBRnWA5mQ==} resolution: {integrity: sha512-y7SAKsQirsEJ2F8bulBck4DoluhI2EEgTimHd6EEUgJBGKy9tC25cpywh1MH4FvDGoG2Unt7+asVd1kj4qOSAw==}
peerDependencies: peerDependencies:
'@vitest/browser': 3.0.7 '@vitest/browser': 3.0.8
vitest: 3.0.7 vitest: 3.0.8
peerDependenciesMeta: peerDependenciesMeta:
'@vitest/browser': '@vitest/browser':
optional: true optional: true
'@vitest/expect@3.0.7': '@vitest/expect@3.0.8':
resolution: {integrity: sha512-QP25f+YJhzPfHrHfYHtvRn+uvkCFCqFtW9CktfBxmB+25QqWsx7VB2As6f4GmwllHLDhXNHvqedwhvMmSnNmjw==} resolution: {integrity: sha512-Xu6TTIavTvSSS6LZaA3EebWFr6tsoXPetOWNMOlc7LO88QVVBwq2oQWBoDiLCN6YTvNYsGSjqOO8CAdjom5DCQ==}
'@vitest/mocker@3.0.7': '@vitest/mocker@3.0.8':
resolution: {integrity: sha512-qui+3BLz9Eonx4EAuR/i+QlCX6AUZ35taDQgwGkK/Tw6/WgwodSrjN1X2xf69IA/643ZX5zNKIn2svvtZDrs4w==} resolution: {integrity: sha512-n3LjS7fcW1BCoF+zWZxG7/5XvuYH+lsFg+BDwwAz0arIwHQJFUEsKBQ0BLU49fCxuM/2HSeBPHQD8WjgrxMfow==}
peerDependencies: peerDependencies:
msw: ^2.4.9 msw: ^2.4.9
vite: ^5.0.0 || ^6.0.0 vite: ^5.0.0 || ^6.0.0
@@ -4854,25 +4854,25 @@ packages:
vite: vite:
optional: true optional: true
'@vitest/pretty-format@3.0.7': '@vitest/pretty-format@3.0.8':
resolution: {integrity: sha512-CiRY0BViD/V8uwuEzz9Yapyao+M9M008/9oMOSQydwbwb+CMokEq3XVaF3XK/VWaOK0Jm9z7ENhybg70Gtxsmg==} resolution: {integrity: sha512-BNqwbEyitFhzYMYHUVbIvepOyeQOSFA/NeJMIP9enMntkkxLgOcgABH6fjyXG85ipTgvero6noreavGIqfJcIg==}
'@vitest/runner@3.0.7': '@vitest/runner@3.0.8':
resolution: {integrity: sha512-WeEl38Z0S2ZcuRTeyYqaZtm4e26tq6ZFqh5y8YD9YxfWuu0OFiGFUbnxNynwLjNRHPsXyee2M9tV7YxOTPZl2g==} resolution: {integrity: sha512-c7UUw6gEcOzI8fih+uaAXS5DwjlBaCJUo7KJ4VvJcjL95+DSR1kova2hFuRt3w41KZEFcOEiq098KkyrjXeM5w==}
'@vitest/snapshot@3.0.7': '@vitest/snapshot@3.0.8':
resolution: {integrity: sha512-eqTUryJWQN0Rtf5yqCGTQWsCFOQe4eNz5Twsu21xYEcnFJtMU5XvmG0vgebhdLlrHQTSq5p8vWHJIeJQV8ovsA==} resolution: {integrity: sha512-x8IlMGSEMugakInj44nUrLSILh/zy1f2/BgH0UeHpNyOocG18M9CWVIFBaXPt8TrqVZWmcPjwfG/ht5tnpba8A==}
'@vitest/spy@3.0.7': '@vitest/spy@3.0.8':
resolution: {integrity: sha512-4T4WcsibB0B6hrKdAZTM37ekuyFZt2cGbEGd2+L0P8ov15J1/HUsUaqkXEQPNAWr4BtPPe1gI+FYfMHhEKfR8w==} resolution: {integrity: sha512-MR+PzJa+22vFKYb934CejhR4BeRpMSoxkvNoDit68GQxRLSf11aT6CTj3XaqUU9rxgWJFnqicN/wxw6yBRkI1Q==}
'@vitest/ui@3.0.7': '@vitest/ui@3.0.8':
resolution: {integrity: sha512-bogkkSaVdSTRj02TfypjrqrLCeEc/tA5V4gAVM843Rp5JtIub3xaij+qjsSnS6CseLQJUSdDCFaFqPMmymRJKQ==} resolution: {integrity: sha512-MfTjaLU+Gw/lYorgwFZ06Cym+Mj9hPfZh/Q91d4JxyAHiicAakPTvS7zYCSHF+5cErwu2PVBe1alSjuh6L/UiA==}
peerDependencies: peerDependencies:
vitest: 3.0.7 vitest: 3.0.8
'@vitest/utils@3.0.7': '@vitest/utils@3.0.8':
resolution: {integrity: sha512-xePVpCRfooFX3rANQjwoditoXgWb1MaFbzmGuPP59MK6i13mrnDw/yEIyJudLeW6/38mCNcwCiJIGmpDPibAIg==} resolution: {integrity: sha512-nkBC3aEhfX2PdtQI/QwAWp8qZWwzASsU4Npbcd5RdMPBSSLCpkZp52P3xku3s3uA0HIEhGvEcF8rNkBsz9dQ4Q==}
'@webassemblyjs/ast@1.12.1': '@webassemblyjs/ast@1.12.1':
resolution: {integrity: sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==} resolution: {integrity: sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==}
@@ -6177,8 +6177,8 @@ packages:
engines: {node: '>=6.0'} engines: {node: '>=6.0'}
hasBin: true hasBin: true
eslint-config-prettier@10.0.2: eslint-config-prettier@10.1.1:
resolution: {integrity: sha512-1105/17ZIMjmCOJOPNfVdbXafLCLj3hPmkmB7dLgt7XsQ/zkxSuDerE/xgO3RxoHysR1N1whmquY0lSn2O0VLg==} resolution: {integrity: sha512-4EQQr6wXwS+ZJSzaR5ZCrYgLxqvUjdXctaEtBqHcbkW944B1NQyO4qpdHQbXBONfwxXdkAY81HH4+LUfrg+zPw==}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
eslint: '>=7.0.0' eslint: '>=7.0.0'
@@ -6448,9 +6448,6 @@ packages:
resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
engines: {node: '>=16'} engines: {node: '>=16'}
flatted@3.3.2:
resolution: {integrity: sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==}
flatted@3.3.3: flatted@3.3.3:
resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
@@ -6914,8 +6911,8 @@ packages:
invariant@2.2.4: invariant@2.2.4:
resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==}
ioredis@5.5.0: ioredis@5.6.0:
resolution: {integrity: sha512-7CutT89g23FfSa8MDoIFs2GYYa0PaNiW/OrT+nRyjRXHDZd17HmIgy+reOQ/yhh72NznNjGuS8kbCAcA4Ro4mw==} resolution: {integrity: sha512-tBZlIIWbndeWBWCXWZiqtOF/yxf6yZX3tAlTJ7nfo5jhd6dctNxF7QnYlZLZ1a0o0pDoen7CgZqO+zjNaFbJAg==}
engines: {node: '>=12.22.0'} engines: {node: '>=12.22.0'}
ip-address@9.0.5: ip-address@9.0.5:
@@ -7653,8 +7650,8 @@ packages:
engines: {node: '>=8', npm: '>=5'} engines: {node: '>=8', npm: '>=5'}
hasBin: true hasBin: true
mysql2@3.12.0: mysql2@3.13.0:
resolution: {integrity: sha512-C8fWhVysZoH63tJbX8d10IAoYCyXy4fdRFz2Ihrt9jtPILYynFEKUUzpp1U7qxzDc3tMbotvaBH+sl6bFnGZiw==} resolution: {integrity: sha512-M6DIQjTqKeqXH5HLbLMxwcK5XfXHw30u5ap6EZmu7QVmcF/gnh2wS/EOiQ4MTbXz/vQeoXrmycPlVRM00WSslg==}
engines: {node: '>= 8.0'} engines: {node: '>= 8.0'}
mz@2.7.0: mz@2.7.0:
@@ -9884,8 +9881,8 @@ packages:
videojs-vtt.js@0.15.5: videojs-vtt.js@0.15.5:
resolution: {integrity: sha512-yZbBxvA7QMYn15Lr/ZfhhLPrNpI/RmCSCqgIff57GC2gIrV5YfyzLfLyZMj0NnZSAz8syB4N0nHXpZg9MyrMOQ==} resolution: {integrity: sha512-yZbBxvA7QMYn15Lr/ZfhhLPrNpI/RmCSCqgIff57GC2gIrV5YfyzLfLyZMj0NnZSAz8syB4N0nHXpZg9MyrMOQ==}
vite-node@3.0.7: vite-node@3.0.8:
resolution: {integrity: sha512-2fX0QwX4GkkkpULXdT1Pf4q0tC1i1lFOyseKoonavXUNlQ77KpW2XqBGGNIm/J4Ows4KxgGJzDguYVPKwG/n5A==} resolution: {integrity: sha512-6PhR4H9VGlcwXZ+KWCdMqbtG649xCPZqfI9j2PsK1FcXgEzro5bGHcVKFCTqPLaNKZES8Evqv4LwvZARsq5qlg==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
hasBin: true hasBin: true
@@ -9928,16 +9925,16 @@ packages:
terser: terser:
optional: true optional: true
vitest@3.0.7: vitest@3.0.8:
resolution: {integrity: sha512-IP7gPK3LS3Fvn44x30X1dM9vtawm0aesAa2yBIZ9vQf+qB69NXC5776+Qmcr7ohUXIQuLhk7xQR0aSUIDPqavg==} resolution: {integrity: sha512-dfqAsNqRGUc8hB9OVR2P0w8PZPEckti2+5rdZip0WIz9WW0MnImJ8XiR61QhqLa92EQzKP2uPkzenKOAHyEIbA==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
'@edge-runtime/vm': '*' '@edge-runtime/vm': '*'
'@types/debug': ^4.1.12 '@types/debug': ^4.1.12
'@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
'@vitest/browser': 3.0.7 '@vitest/browser': 3.0.8
'@vitest/ui': 3.0.7 '@vitest/ui': 3.0.8
happy-dom: '*' happy-dom: '*'
jsdom: '*' jsdom: '*'
peerDependenciesMeta: peerDependenciesMeta:
@@ -12323,25 +12320,25 @@ snapshots:
dependencies: dependencies:
remove-accents: 0.5.0 remove-accents: 0.5.0
'@tanstack/query-core@5.67.1': {} '@tanstack/query-core@5.67.2': {}
'@tanstack/query-devtools@5.65.0': {} '@tanstack/query-devtools@5.67.2': {}
'@tanstack/react-query-devtools@5.67.1(@tanstack/react-query@5.67.1(react@19.0.0))(react@19.0.0)': '@tanstack/react-query-devtools@5.67.2(@tanstack/react-query@5.67.2(react@19.0.0))(react@19.0.0)':
dependencies: dependencies:
'@tanstack/query-devtools': 5.65.0 '@tanstack/query-devtools': 5.67.2
'@tanstack/react-query': 5.67.1(react@19.0.0) '@tanstack/react-query': 5.67.2(react@19.0.0)
react: 19.0.0 react: 19.0.0
'@tanstack/react-query-next-experimental@5.67.1(@tanstack/react-query@5.67.1(react@19.0.0))(next@15.1.7(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.85.1))(react@19.0.0)': '@tanstack/react-query-next-experimental@5.67.2(@tanstack/react-query@5.67.2(react@19.0.0))(next@15.1.7(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.85.1))(react@19.0.0)':
dependencies: dependencies:
'@tanstack/react-query': 5.67.1(react@19.0.0) '@tanstack/react-query': 5.67.2(react@19.0.0)
next: 15.1.7(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.85.1) next: 15.1.7(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.85.1)
react: 19.0.0 react: 19.0.0
'@tanstack/react-query@5.67.1(react@19.0.0)': '@tanstack/react-query@5.67.2(react@19.0.0)':
dependencies: dependencies:
'@tanstack/query-core': 5.67.1 '@tanstack/query-core': 5.67.2
react: 19.0.0 react: 19.0.0
'@tanstack/react-table@8.20.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': '@tanstack/react-table@8.20.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
@@ -12588,7 +12585,7 @@ snapshots:
'@trpc/server': 11.0.0-rc.824(typescript@5.8.2) '@trpc/server': 11.0.0-rc.824(typescript@5.8.2)
typescript: 5.8.2 typescript: 5.8.2
'@trpc/next@11.0.0-rc.824(@tanstack/react-query@5.67.1(react@19.0.0))(@trpc/client@11.0.0-rc.824(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(typescript@5.8.2))(@trpc/react-query@11.0.0-rc.824(@tanstack/react-query@5.67.1(react@19.0.0))(@trpc/client@11.0.0-rc.824(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(typescript@5.8.2))(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.8.2))(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(next@15.1.7(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.85.1))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.8.2)': '@trpc/next@11.0.0-rc.824(@tanstack/react-query@5.67.2(react@19.0.0))(@trpc/client@11.0.0-rc.824(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(typescript@5.8.2))(@trpc/react-query@11.0.0-rc.824(@tanstack/react-query@5.67.2(react@19.0.0))(@trpc/client@11.0.0-rc.824(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(typescript@5.8.2))(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.8.2))(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(next@15.1.7(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.85.1))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.8.2)':
dependencies: dependencies:
'@trpc/client': 11.0.0-rc.824(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(typescript@5.8.2) '@trpc/client': 11.0.0-rc.824(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(typescript@5.8.2)
'@trpc/server': 11.0.0-rc.824(typescript@5.8.2) '@trpc/server': 11.0.0-rc.824(typescript@5.8.2)
@@ -12597,12 +12594,12 @@ snapshots:
react-dom: 19.0.0(react@19.0.0) react-dom: 19.0.0(react@19.0.0)
typescript: 5.8.2 typescript: 5.8.2
optionalDependencies: optionalDependencies:
'@tanstack/react-query': 5.67.1(react@19.0.0) '@tanstack/react-query': 5.67.2(react@19.0.0)
'@trpc/react-query': 11.0.0-rc.824(@tanstack/react-query@5.67.1(react@19.0.0))(@trpc/client@11.0.0-rc.824(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(typescript@5.8.2))(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.8.2) '@trpc/react-query': 11.0.0-rc.824(@tanstack/react-query@5.67.2(react@19.0.0))(@trpc/client@11.0.0-rc.824(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(typescript@5.8.2))(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.8.2)
'@trpc/react-query@11.0.0-rc.824(@tanstack/react-query@5.67.1(react@19.0.0))(@trpc/client@11.0.0-rc.824(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(typescript@5.8.2))(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.8.2)': '@trpc/react-query@11.0.0-rc.824(@tanstack/react-query@5.67.2(react@19.0.0))(@trpc/client@11.0.0-rc.824(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(typescript@5.8.2))(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.8.2)':
dependencies: dependencies:
'@tanstack/react-query': 5.67.1(react@19.0.0) '@tanstack/react-query': 5.67.2(react@19.0.0)
'@trpc/client': 11.0.0-rc.824(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(typescript@5.8.2) '@trpc/client': 11.0.0-rc.824(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(typescript@5.8.2)
'@trpc/server': 11.0.0-rc.824(typescript@5.8.2) '@trpc/server': 11.0.0-rc.824(typescript@5.8.2)
react: 19.0.0 react: 19.0.0
@@ -12993,7 +12990,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@vitest/coverage-v8@3.0.7(vitest@3.0.7)': '@vitest/coverage-v8@3.0.8(vitest@3.0.8)':
dependencies: dependencies:
'@ampproject/remapping': 2.3.0 '@ampproject/remapping': 2.3.0
'@bcoe/v8-coverage': 1.0.2 '@bcoe/v8-coverage': 1.0.2
@@ -13007,58 +13004,58 @@ snapshots:
std-env: 3.8.0 std-env: 3.8.0
test-exclude: 7.0.1 test-exclude: 7.0.1
tinyrainbow: 2.0.0 tinyrainbow: 2.0.0
vitest: 3.0.7(@types/node@22.13.9)(@vitest/ui@3.0.7)(jsdom@26.0.0)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0) vitest: 3.0.8(@types/node@22.13.9)(@vitest/ui@3.0.8)(jsdom@26.0.0)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@vitest/expect@3.0.7': '@vitest/expect@3.0.8':
dependencies: dependencies:
'@vitest/spy': 3.0.7 '@vitest/spy': 3.0.8
'@vitest/utils': 3.0.7 '@vitest/utils': 3.0.8
chai: 5.2.0 chai: 5.2.0
tinyrainbow: 2.0.0 tinyrainbow: 2.0.0
'@vitest/mocker@3.0.7(vite@5.4.5(@types/node@22.13.9)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0))': '@vitest/mocker@3.0.8(vite@5.4.5(@types/node@22.13.9)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0))':
dependencies: dependencies:
'@vitest/spy': 3.0.7 '@vitest/spy': 3.0.8
estree-walker: 3.0.3 estree-walker: 3.0.3
magic-string: 0.30.17 magic-string: 0.30.17
optionalDependencies: optionalDependencies:
vite: 5.4.5(@types/node@22.13.9)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0) vite: 5.4.5(@types/node@22.13.9)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0)
'@vitest/pretty-format@3.0.7': '@vitest/pretty-format@3.0.8':
dependencies: dependencies:
tinyrainbow: 2.0.0 tinyrainbow: 2.0.0
'@vitest/runner@3.0.7': '@vitest/runner@3.0.8':
dependencies: dependencies:
'@vitest/utils': 3.0.7 '@vitest/utils': 3.0.8
pathe: 2.0.3 pathe: 2.0.3
'@vitest/snapshot@3.0.7': '@vitest/snapshot@3.0.8':
dependencies: dependencies:
'@vitest/pretty-format': 3.0.7 '@vitest/pretty-format': 3.0.8
magic-string: 0.30.17 magic-string: 0.30.17
pathe: 2.0.3 pathe: 2.0.3
'@vitest/spy@3.0.7': '@vitest/spy@3.0.8':
dependencies: dependencies:
tinyspy: 3.0.2 tinyspy: 3.0.2
'@vitest/ui@3.0.7(vitest@3.0.7)': '@vitest/ui@3.0.8(vitest@3.0.8)':
dependencies: dependencies:
'@vitest/utils': 3.0.7 '@vitest/utils': 3.0.8
fflate: 0.8.2 fflate: 0.8.2
flatted: 3.3.3 flatted: 3.3.3
pathe: 2.0.3 pathe: 2.0.3
sirv: 3.0.1 sirv: 3.0.1
tinyglobby: 0.2.12 tinyglobby: 0.2.12
tinyrainbow: 2.0.0 tinyrainbow: 2.0.0
vitest: 3.0.7(@types/node@22.13.9)(@vitest/ui@3.0.7)(jsdom@26.0.0)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0) vitest: 3.0.8(@types/node@22.13.9)(@vitest/ui@3.0.8)(jsdom@26.0.0)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0)
'@vitest/utils@3.0.7': '@vitest/utils@3.0.8':
dependencies: dependencies:
'@vitest/pretty-format': 3.0.7 '@vitest/pretty-format': 3.0.8
loupe: 3.1.3 loupe: 3.1.3
tinyrainbow: 2.0.0 tinyrainbow: 2.0.0
@@ -14194,17 +14191,17 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
drizzle-orm@0.40.0(@libsql/client-wasm@0.14.0)(@types/better-sqlite3@7.6.12)(better-sqlite3@11.8.1)(gel@2.0.0)(mysql2@3.12.0): drizzle-orm@0.40.0(@libsql/client-wasm@0.14.0)(@types/better-sqlite3@7.6.12)(better-sqlite3@11.8.1)(gel@2.0.0)(mysql2@3.13.0):
optionalDependencies: optionalDependencies:
'@libsql/client-wasm': 0.14.0 '@libsql/client-wasm': 0.14.0
'@types/better-sqlite3': 7.6.12 '@types/better-sqlite3': 7.6.12
better-sqlite3: 11.8.1 better-sqlite3: 11.8.1
gel: 2.0.0 gel: 2.0.0
mysql2: 3.12.0 mysql2: 3.13.0
drizzle-zod@0.7.0(drizzle-orm@0.40.0(@libsql/client-wasm@0.14.0)(@types/better-sqlite3@7.6.12)(better-sqlite3@11.8.1)(gel@2.0.0)(mysql2@3.12.0))(zod@3.24.2): drizzle-zod@0.7.0(drizzle-orm@0.40.0(@libsql/client-wasm@0.14.0)(@types/better-sqlite3@7.6.12)(better-sqlite3@11.8.1)(gel@2.0.0)(mysql2@3.13.0))(zod@3.24.2):
dependencies: dependencies:
drizzle-orm: 0.40.0(@libsql/client-wasm@0.14.0)(@types/better-sqlite3@7.6.12)(better-sqlite3@11.8.1)(gel@2.0.0)(mysql2@3.12.0) drizzle-orm: 0.40.0(@libsql/client-wasm@0.14.0)(@types/better-sqlite3@7.6.12)(better-sqlite3@11.8.1)(gel@2.0.0)(mysql2@3.13.0)
zod: 3.24.2 zod: 3.24.2
dunder-proto@1.0.1: dunder-proto@1.0.1:
@@ -14600,7 +14597,7 @@ snapshots:
optionalDependencies: optionalDependencies:
source-map: 0.6.1 source-map: 0.6.1
eslint-config-prettier@10.0.2(eslint@9.21.0): eslint-config-prettier@10.1.1(eslint@9.21.0):
dependencies: dependencies:
eslint: 9.21.0 eslint: 9.21.0
@@ -14940,11 +14937,9 @@ snapshots:
flat-cache@4.0.1: flat-cache@4.0.1:
dependencies: dependencies:
flatted: 3.3.2 flatted: 3.3.3
keyv: 4.5.4 keyv: 4.5.4
flatted@3.3.2: {}
flatted@3.3.3: {} flatted@3.3.3: {}
fn.name@1.1.0: {} fn.name@1.1.0: {}
@@ -15478,7 +15473,7 @@ snapshots:
dependencies: dependencies:
loose-envify: 1.4.0 loose-envify: 1.4.0
ioredis@5.5.0: ioredis@5.6.0:
dependencies: dependencies:
'@ioredis/commands': 1.2.0 '@ioredis/commands': 1.2.0
cluster-key-slot: 1.1.2 cluster-key-slot: 1.1.2
@@ -16191,7 +16186,7 @@ snapshots:
'@babel/runtime': 7.25.6 '@babel/runtime': 7.25.6
global: 4.4.0 global: 4.4.0
mysql2@3.12.0: mysql2@3.13.0:
dependencies: dependencies:
aws-ssl-profiles: 1.1.2 aws-ssl-profiles: 1.1.2
denque: 2.1.0 denque: 2.1.0
@@ -18687,7 +18682,7 @@ snapshots:
dependencies: dependencies:
global: 4.4.0 global: 4.4.0
vite-node@3.0.7(@types/node@22.13.9)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0): vite-node@3.0.8(@types/node@22.13.9)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0):
dependencies: dependencies:
cac: 6.7.14 cac: 6.7.14
debug: 4.4.0 debug: 4.4.0
@@ -18728,15 +18723,15 @@ snapshots:
sugarss: 4.0.1(postcss@8.4.47) sugarss: 4.0.1(postcss@8.4.47)
terser: 5.32.0 terser: 5.32.0
vitest@3.0.7(@types/node@22.13.9)(@vitest/ui@3.0.7)(jsdom@26.0.0)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0): vitest@3.0.8(@types/node@22.13.9)(@vitest/ui@3.0.8)(jsdom@26.0.0)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0):
dependencies: dependencies:
'@vitest/expect': 3.0.7 '@vitest/expect': 3.0.8
'@vitest/mocker': 3.0.7(vite@5.4.5(@types/node@22.13.9)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0)) '@vitest/mocker': 3.0.8(vite@5.4.5(@types/node@22.13.9)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0))
'@vitest/pretty-format': 3.0.7 '@vitest/pretty-format': 3.0.8
'@vitest/runner': 3.0.7 '@vitest/runner': 3.0.8
'@vitest/snapshot': 3.0.7 '@vitest/snapshot': 3.0.8
'@vitest/spy': 3.0.7 '@vitest/spy': 3.0.8
'@vitest/utils': 3.0.7 '@vitest/utils': 3.0.8
chai: 5.2.0 chai: 5.2.0
debug: 4.4.0 debug: 4.4.0
expect-type: 1.1.0 expect-type: 1.1.0
@@ -18748,11 +18743,11 @@ snapshots:
tinypool: 1.0.2 tinypool: 1.0.2
tinyrainbow: 2.0.0 tinyrainbow: 2.0.0
vite: 5.4.5(@types/node@22.13.9)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0) vite: 5.4.5(@types/node@22.13.9)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0)
vite-node: 3.0.7(@types/node@22.13.9)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0) vite-node: 3.0.8(@types/node@22.13.9)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0)
why-is-node-running: 2.3.0 why-is-node-running: 2.3.0
optionalDependencies: optionalDependencies:
'@types/node': 22.13.9 '@types/node': 22.13.9
'@vitest/ui': 3.0.7(vitest@3.0.7) '@vitest/ui': 3.0.8(vitest@3.0.8)
jsdom: 26.0.0 jsdom: 26.0.0
transitivePeerDependencies: transitivePeerDependencies:
- less - less

View File

@@ -18,7 +18,7 @@
"prettier": "@homarr/prettier-config", "prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@next/eslint-plugin-next": "15.1.7", "@next/eslint-plugin-next": "15.1.7",
"eslint-config-prettier": "^10.0.2", "eslint-config-prettier": "^10.1.1",
"eslint-config-turbo": "^2.4.4", "eslint-config-turbo": "^2.4.4",
"eslint-plugin-import": "^2.31.0", "eslint-plugin-import": "^2.31.0",
"eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-jsx-a11y": "^6.10.2",