-
-
+
{direction === "row" ? (
-
+
) : (
-
+
)}
@@ -117,29 +134,27 @@ const FlexLayout = ({ data, direction, hideIcon, hideHostname, openNewTab, hasIc
interface GridLayoutProps {
data: RouterOutputs["app"]["byIds"];
- width: number;
- height: number;
+ hideTitle: boolean;
hideIcon: boolean;
hideHostname: boolean;
openNewTab: boolean;
+ itemDirection: "horizontal" | "vertical";
hasIconColor: boolean;
}
-const GridLayout = ({ data, width, height, hideIcon, hideHostname, openNewTab, hasIconColor }: GridLayoutProps) => {
- // 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 GridLayout = ({
+ data,
+ hideTitle,
+ hideIcon,
+ hideHostname,
+ openNewTab,
+ itemDirection,
+ hasIconColor,
+}: GridLayoutProps) => {
const board = useRequiredBoard();
return (
-
+
{data.map((app) => (
-
+ {itemDirection === "horizontal" ? (
+
+ ) : (
+
+ )}
))}
-
+
);
};
const VerticalItem = ({
app,
+ hideTitle,
hideIcon,
hideHostname,
hasIconColor,
- size = 30,
}: {
app: RouterOutputs["app"]["byIds"][number];
+ hideTitle: boolean;
hideIcon: boolean;
hideHostname: boolean;
hasIconColor: boolean;
- size?: number;
}) => {
return (
-
- {app.name}
-
+ {!hideTitle && (
+
+ {app.name}
+
+ )}
{!hideIcon && (
)}
{!hideHostname && (
-
+
{app.href ? new URL(app.href).hostname : undefined}
)}
@@ -215,17 +241,19 @@ const VerticalItem = ({
const HorizontalItem = ({
app,
+ hideTitle,
hideIcon,
hideHostname,
hasIconColor,
}: {
app: RouterOutputs["app"]["byIds"][number];
+ hideTitle: boolean;
hideIcon: boolean;
hideHostname: boolean;
hasIconColor: boolean;
}) => {
return (
-
+
{!hideIcon && (
)}
-
-
- {app.name}
-
+ {!(hideTitle && hideHostname) && (
+ <>
+
+ {!hideTitle && (
+
+ {app.name}
+
+ )}
- {!hideHostname && (
-
- {app.href ? new URL(app.href).hostname : undefined}
-
- )}
-
+ {!hideHostname && (
+
+ {app.href ? new URL(app.href).hostname : undefined}
+
+ )}
+
+ >
+ )}
);
};
diff --git a/packages/widgets/src/bookmarks/index.tsx b/packages/widgets/src/bookmarks/index.tsx
index 763b0a3ad..37eaa9450 100644
--- a/packages/widgets/src/bookmarks/index.tsx
+++ b/packages/widgets/src/bookmarks/index.tsx
@@ -14,12 +14,13 @@ export const { definition, componentLoader } = createWidgetDefinition("bookmarks
return optionsBuilder.from((factory) => ({
title: factory.text(),
layout: factory.select({
- options: (["grid", "row", "column"] as const).map((value) => ({
+ options: (["grid", "gridHorizontal", "row", "column"] as const).map((value) => ({
value,
label: (t) => t(`widget.bookmarks.option.layout.option.${value}.label`),
})),
defaultValue: "column",
}),
+ hideTitle: factory.switch({ defaultValue: false }),
hideIcon: factory.switch({ defaultValue: false }),
hideHostname: factory.switch({ defaultValue: false }),
openNewTab: factory.switch({ defaultValue: true }),
diff --git a/packages/widgets/src/calendar/calendar-event-list.tsx b/packages/widgets/src/calendar/calendar-event-list.tsx
index 22eaa8660..2b661ab72 100644
--- a/packages/widgets/src/calendar/calendar-event-list.tsx
+++ b/packages/widgets/src/calendar/calendar-event-list.tsx
@@ -30,7 +30,7 @@ export const CalendarEventList = ({ events }: CalendarEventListProps) => {
{
{events.map((event, eventIndex) => (
-
+
{event.mediaInformation?.type === "tv" && (
{date.getDate()}
- {rootHeight >= 350 && }
+
-
+ {/* Popover has some offset on the left side, padding is removed because of scrollarea paddings */}
+
@@ -72,14 +73,17 @@ export const CalendarDay = ({ date, events, disabled, rootHeight, rootWidth }: C
interface NotificationIndicatorProps {
events: CalendarEvent[];
+ rootHeight: number;
}
-const NotificationIndicator = ({ events }: NotificationIndicatorProps) => {
+const NotificationIndicator = ({ events, rootHeight }: NotificationIndicatorProps) => {
+ const isSmall = rootHeight < 256;
const notificationEvents = [...new Set(events.map((event) => event.links[0]?.notificationColor))].filter(String);
+ /* position bottom is lower when small to not be on top of number*/
return (
-
+
{notificationEvents.map((notificationEvent) => {
- return ;
+ return ;
})}
);
diff --git a/packages/widgets/src/calendar/component.tsx b/packages/widgets/src/calendar/component.tsx
index d05bbc7d8..064067b30 100644
--- a/packages/widgets/src/calendar/component.tsx
+++ b/packages/widgets/src/calendar/component.tsx
@@ -39,6 +39,7 @@ const FetchCalendar = ({ month, setMonth, isEditMode, integrationIds, options }:
month: month.getMonth(),
year: month.getFullYear(),
releaseType: options.releaseType,
+ showUnmonitored: options.showUnmonitored,
},
{
refetchOnMount: false,
@@ -105,8 +106,8 @@ const CalendarBase = ({ isEditMode, events, month, setMonth, options }: Calendar
},
day: {
borderRadius: actualItemRadius,
- width: width < 350 ? 20 : 50,
- height: height < 350 ? 20 : 50,
+ width: "100%",
+ height: "100%",
},
month: {
height: "100%",
diff --git a/packages/widgets/src/calendar/index.ts b/packages/widgets/src/calendar/index.ts
index 275b2d605..088bdf6ef 100644
--- a/packages/widgets/src/calendar/index.ts
+++ b/packages/widgets/src/calendar/index.ts
@@ -26,6 +26,9 @@ export const { definition, componentLoader } = createWidgetDefinition("calendar"
validate: z.number().min(2).max(9999),
defaultValue: 2,
}),
+ showUnmonitored: factory.switch({
+ defaultValue: false,
+ }),
}));
},
supportedIntegrations: getIntegrationKindsByCategory("calendar"),
diff --git a/packages/widgets/src/clock/component.tsx b/packages/widgets/src/clock/component.tsx
index 32cf45549..417585162 100644
--- a/packages/widgets/src/clock/component.tsx
+++ b/packages/widgets/src/clock/component.tsx
@@ -13,28 +13,31 @@ dayjs.extend(advancedFormat);
dayjs.extend(utc);
dayjs.extend(timezones);
-export default function ClockWidget({ options }: WidgetComponentProps<"clock">) {
+export default function ClockWidget({ options, width }: WidgetComponentProps<"clock">) {
const secondsFormat = options.showSeconds ? ":ss" : "";
- const timeFormat = options.is24HourFormat ? `HH:mm${secondsFormat}` : `h:mm${secondsFormat} A`;
+ const timeFormat = options.is24HourFormat ? `HH:mm${secondsFormat}` : `hh:mm${secondsFormat} A`;
const dateFormat = options.dateFormat;
const customTimeFormat = options.customTimeFormat;
const customDateFormat = options.customDateFormat;
const timezone = options.useCustomTimezone ? options.timezone : Intl.DateTimeFormat().resolvedOptions().timeZone;
const time = useCurrentTime(options);
+
+ const sizing = width < 128 ? "xs" : width < 196 ? "sm" : "md";
+
return (
-
+
{options.customTitleToggle && (
-
+
{options.customTitle}
)}
-
+
{options.customTimeFormat
? dayjs(time).tz(timezone).format(customTimeFormat)
: dayjs(time).tz(timezone).format(timeFormat)}
{options.showDate && (
-
+
{options.customDateFormat
? dayjs(time).tz(timezone).format(customDateFormat)
: dayjs(time).tz(timezone).format(dateFormat)}
diff --git a/packages/widgets/src/dns-hole/controls/component.tsx b/packages/widgets/src/dns-hole/controls/component.tsx
index 7ee3576a4..8ce9251a4 100644
--- a/packages/widgets/src/dns-hole/controls/component.tsx
+++ b/packages/widgets/src/dns-hole/controls/component.tsx
@@ -3,7 +3,20 @@
import "../../widgets-common.css";
import { useState } from "react";
-import { ActionIcon, Badge, Button, Card, Flex, ScrollArea, Stack, Text, Tooltip, UnstyledButton } from "@mantine/core";
+import {
+ ActionIcon,
+ Badge,
+ Button,
+ Card,
+ Flex,
+ Group,
+ Indicator,
+ ScrollArea,
+ Stack,
+ Text,
+ Tooltip,
+ UnstyledButton,
+} from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { IconCircleFilled, IconClockPause, IconPlayerPlay, IconPlayerStop } from "@tabler/icons-react";
import combineClasses from "clsx";
@@ -30,6 +43,7 @@ export default function DnsHoleControlsWidget({
options,
integrationIds,
isEditMode,
+ width,
}: WidgetComponentProps) {
const board = useRequiredBoard();
// DnsHole integrations with interaction permissions
@@ -177,10 +191,10 @@ export default function DnsHoleControlsWidget({
const controlAllButtonsVisible = options.showToggleAllButtons && integrationsWithInteractions.length > 0;
return (
-
)}
-
+
))}
-
+
-
+
);
}
@@ -283,6 +298,7 @@ interface ControlsCardProps {
open: () => void;
t: TranslationFunction;
hasIconColor: boolean;
+ rootWidth: number;
}
const ControlsCard: React.FC
= ({
@@ -293,6 +309,7 @@ const ControlsCard: React.FC = ({
open,
t,
hasIconColor,
+ rootWidth,
}) => {
const isConnected = useIntegrationConnected(data.integration.updatedAt, { timeout: 30000 });
const isEnabled = data.summary.status ? data.summary.status === "enabled" : undefined;
@@ -302,95 +319,161 @@ const ControlsCard: React.FC = ({
const board = useRequiredBoard();
const iconUrl = integrationDefs[data.integration.kind].iconUrl;
+ const layout = rootWidth < 256 ? "sm" : "md";
return (
-
-
-
-
-
- {data.integration.name}
-
-
- toggleDns(data.integration.id)}
- >
-
- )
- }
- >
- {t(
- `widget.dnsHoleControls.controls.${
- !isConnected
- ? "disconnected"
- : typeof isEnabled === "undefined"
- ? "processing"
- : isEnabled
- ? "enabled"
- : "disabled"
- }`,
- )}
-
-
+
+
+ {layout === "md" && (
+
+ )}
+
+
+
+ {layout === "sm" && (
+
+ )}
+
+ {data.integration.name}
+
+
+
+ {layout === "sm" && (
+
+ {!isEnabled ? (
+ toggleDns(data.integration.id)}
+ disabled={!controlEnabled}
+ size="sm"
+ color="green"
+ variant="light"
+ >
+
+
+ ) : (
+ toggleDns(data.integration.id)}
+ disabled={!controlEnabled}
+ size="sm"
+ color="red"
+ variant="light"
+ >
+
+
+ )}
+ {
+ setSelectedIntegrationIds([data.integration.id]);
+ open();
+ }}
+ size="sm"
+ color="yellow"
+ variant="light"
+ >
+
+
+
+ )}
+ {layout === "md" && (
+ toggleDns(data.integration.id)}
+ >
+
+ )
+ }
+ >
+ {t(
+ `widget.dnsHoleControls.controls.${
+ !isConnected
+ ? "disconnected"
+ : typeof isEnabled === "undefined"
+ ? "processing"
+ : isEnabled
+ ? "enabled"
+ : "disabled"
+ }`,
+ )}
+
+
+ )}
+
+ {layout === "md" && (
+ {
+ setSelectedIntegrationIds([data.integration.id]);
+ open();
+ }}
+ >
+
+
+ )}
- {
- setSelectedIntegrationIds([data.integration.id]);
- open();
- }}
- >
-
-
-
-
+
+
);
};
diff --git a/packages/widgets/src/dns-hole/summary/component.tsx b/packages/widgets/src/dns-hole/summary/component.tsx
index 239d02295..b0636ff44 100644
--- a/packages/widgets/src/dns-hole/summary/component.tsx
+++ b/packages/widgets/src/dns-hole/summary/component.tsx
@@ -63,7 +63,7 @@ export default function DnsHoleSummaryWidget({ options, integrationIds }: Widget
const data = useMemo(() => summaries.flatMap(({ summary }) => summary), [summaries]);
return (
-
+
{data.length > 0 ? (
stats.map((item) => (
@@ -89,43 +89,43 @@ export default function DnsHoleSummaryWidget({ options, integrationIds }: Widget
const stats = [
{
icon: IconBarrierBlock,
- value: (data) =>
+ value: (data, size) =>
formatNumber(
data.reduce((count, { adsBlockedToday }) => count + adsBlockedToday, 0),
- 2,
+ size === "sm" ? 0 : 2,
),
label: (t) => t("widget.dnsHoleSummary.data.adsBlockedToday"),
color: "rgba(240, 82, 60, 0.4)", // RED
},
{
icon: IconPercentage,
- value: (data) => {
+ value: (data, size) => {
const totalCount = data.reduce((count, { dnsQueriesToday }) => count + dnsQueriesToday, 0);
const blocked = data.reduce((count, { adsBlockedToday }) => count + adsBlockedToday, 0);
- return `${formatNumber(totalCount === 0 ? 0 : (blocked / totalCount) * 100, 2)}%`;
+ return `${formatNumber(totalCount === 0 ? 0 : (blocked / totalCount) * 100, size === "sm" ? 0 : 2)}%`;
},
label: (t) => t("widget.dnsHoleSummary.data.adsBlockedTodayPercentage"),
color: "rgba(255, 165, 20, 0.4)", // YELLOW
},
{
icon: IconSearch,
- value: (data) =>
+ value: (data, size) =>
formatNumber(
data.reduce((count, { dnsQueriesToday }) => count + dnsQueriesToday, 0),
- 2,
+ size === "sm" ? 0 : 2,
),
label: (t) => t("widget.dnsHoleSummary.data.dnsQueriesToday"),
color: "rgba(0, 175, 218, 0.4)", // BLUE
},
{
icon: IconWorldWww,
- value: (data) => {
+ value: (data, size) => {
// We use a suffix to indicate that there might be more domains in the at least two lists.
const suffix = data.length >= 2 ? "+" : "";
return (
formatNumber(
data.reduce((count, { domainsBeingBlocked }) => count + domainsBeingBlocked, 0),
- 2,
+ size === "sm" ? 0 : 2,
) + suffix
);
},
@@ -137,7 +137,7 @@ const stats = [
interface StatItem {
icon: TablerIcon;
- value: (summaries: DnsHoleSummary[]) => string;
+ value: (summaries: DnsHoleSummary[], size: "sm" | "md") => string;
tooltip?: (summaries: DnsHoleSummary[], t: TranslationFunction) => string | undefined;
label: stringOrTranslation;
color: string;
@@ -152,6 +152,8 @@ interface StatCardProps {
const StatCard = ({ item, data, usePiHoleColors, t }: StatCardProps) => {
const { ref, height, width } = useElementSize();
const isLong = width > height + 20;
+ const canStackText = height > 32;
+ const hideLabel = (height <= 32 && width <= 256) || (height <= 64 && width <= 92);
const tooltip = item.tooltip?.(data, t);
const board = useRequiredBoard();
@@ -174,25 +176,26 @@ const StatCard = ({ item, data, usePiHoleColors, t }: StatCardProps) => {
align="center"
justify="center"
direction={isLong ? "row" : "column"}
- style={{ containerType: "size" }}
+ gap={0}
>
-
+
-
- {item.value(data)}
+
+ {item.value(data, width <= 64 ? "sm" : "md")}
- {item.label && (
-
+ {!hideLabel && (
+
{translateIfNecessary(t, item.label)}
)}
diff --git a/packages/widgets/src/downloads/component.tsx b/packages/widgets/src/downloads/component.tsx
index 10c98ba8a..588cdf608 100644
--- a/packages/widgets/src/downloads/component.tsx
+++ b/packages/widgets/src/downloads/component.tsx
@@ -24,7 +24,6 @@ import {
Tooltip,
} from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
-import type { IconProps } from "@tabler/icons-react";
import {
IconAlertTriangle,
IconCirclesRelation,
@@ -75,16 +74,6 @@ const columnsRatios: Record = {
upSpeed: 3,
};
-const actionIconIconStyle: IconProps["style"] = {
- height: "var(--ai-icon-size)",
- width: "var(--ai-icon-size)",
-};
-
-const standardIconStyle: IconProps["style"] = {
- height: "var(--icon-size)",
- width: "var(--icon-size)",
-};
-
export default function DownloadClientsWidget({
isEditMode,
integrationIds,
@@ -307,72 +296,41 @@ export default function DownloadClientsWidget({
upSpeed: options.columns.includes("upSpeed") && integrationTypes.includes("torrent"),
} satisfies Record;
- //Set a relative width using ratio table
- const totalWidth = options.columns.reduce(
- (count: number, column) => (columnVisibility[column] ? count + columnsRatios[column] : count),
- 0,
- );
-
//Default styling behavior for stopping interaction when editing. (Applied everywhere except the table header)
const editStyle: MantineStyleProp = {
pointerEvents: isEditMode ? "none" : undefined,
};
- //General style sizing as vars that should apply or be applied to all elements
- const baseStyle: MantineStyleProp = {
- "--total-width": totalWidth,
- "--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
- "--button-fz": "var(--text-fz)",
- "--icon-size": "calc(var(--ratio-width) * 2 / 3)", //Normal icon size
- "--ai-icon-size": "calc(var(--ratio-width) * 0.5)", //Icon inside action icons size
- "--button-size": "calc(var(--ratio-width) * 0.75)", //Action Icon, button and avatar size
- "--image-size": "var(--button-size)",
- "--mrt-base-background-color": "transparent",
- };
-
//Base element in common with all columns
const columnsDefBase = useCallback(
({
key,
showHeader,
- align,
}: {
key: keyof ExtendedDownloadClientItem;
showHeader: boolean;
- align?: "center" | "left" | "right" | "justify" | "char";
}): MRT_ColumnDef => {
- const style: MantineStyleProp = {
- minWidth: 0,
- width: "var(--column-width)",
- height: "var(--ratio-width)",
- padding: "var(--space-size)",
- transition: "unset",
- "--key-width": columnsRatios[key],
- "--column-width": "calc((var(--key-width)/var(--total-width) * 100cqw))",
- };
return {
id: key,
accessorKey: key,
header: key,
size: columnsRatios[key],
- mantineTableBodyCellProps: { style, align },
- mantineTableHeadCellProps: {
- style,
- align: isEditMode ? "center" : align,
- },
- Header: () => (showHeader && !isEditMode ? {t(`items.${key}.columnTitle`)} : ""),
+ Header: () =>
+ showHeader ? (
+
+ {t(`items.${key}.columnTitle`)}
+
+ ) : null,
};
},
- [isEditMode, t],
+ [t],
);
//Make columns and cell elements, Memoized to data with deps on data and EditMode
const columns = useMemo[]>(
() => [
{
- ...columnsDefBase({ key: "actions", showHeader: false, align: "center" }),
+ ...columnsDefBase({ key: "actions", showHeader: false }),
enableSorting: false,
Cell: ({ cell, row }) => {
const actions = cell.getValue();
@@ -380,19 +338,15 @@ export default function DownloadClientsWidget({
const [opened, { open, close }] = useDisclosure(false);
return actions ? (
-
+
-
- {pausedAction === "resume" ? (
-
- ) : (
-
- )}
+
+ {pausedAction === "resume" ? : }
-
-
+
+
@@ -423,68 +377,68 @@ export default function DownloadClientsWidget({
) : (
-
-
+
+
);
},
},
{
- ...columnsDefBase({ key: "added", showHeader: true, align: "center" }),
+ ...columnsDefBase({ key: "added", showHeader: true }),
sortUndefined: "last",
Cell: ({ cell }) => {
const added = cell.getValue();
- return {added !== undefined ? dayjs(added).fromNow() : "unknown"};
+ return {added !== undefined ? dayjs(added).fromNow() : "unknown"};
},
},
{
- ...columnsDefBase({ key: "category", showHeader: false, align: "center" }),
+ ...columnsDefBase({ key: "category", showHeader: false }),
sortUndefined: "last",
Cell: ({ cell }) => {
const category = cell.getValue();
return (
category !== undefined && (
-
+
)
);
},
},
{
- ...columnsDefBase({ key: "downSpeed", showHeader: true, align: "right" }),
+ ...columnsDefBase({ key: "downSpeed", showHeader: true }),
sortUndefined: "last",
Cell: ({ cell }) => {
const downSpeed = cell.getValue();
- return downSpeed && {humanFileSize(downSpeed, "/s")};
+ return downSpeed ? {humanFileSize(downSpeed, "/s")} : null;
},
},
{
- ...columnsDefBase({ key: "id", showHeader: false, align: "center" }),
+ ...columnsDefBase({ key: "id", showHeader: false }),
enableSorting: false,
Cell: ({ cell }) => {
const id = cell.getValue();
return (
-
+
);
},
},
{
- ...columnsDefBase({ key: "index", showHeader: true, align: "center" }),
+ ...columnsDefBase({ key: "index", showHeader: true }),
Cell: ({ cell }) => {
const index = cell.getValue();
- return {index};
+ return {index};
},
},
{
- ...columnsDefBase({ key: "integration", showHeader: false, align: "center" }),
+ ...columnsDefBase({ key: "integration", showHeader: false }),
Cell: ({ cell }) => {
const integration = cell.getValue();
return (
-
+
);
},
@@ -494,62 +448,61 @@ export default function DownloadClientsWidget({
Cell: ({ cell }) => {
const name = cell.getValue();
return (
-
+
{name}
);
},
},
{
- ...columnsDefBase({ key: "progress", showHeader: true, align: "center" }),
+ ...columnsDefBase({ key: "progress", showHeader: true }),
Cell: ({ cell, row }) => {
const progress = cell.getValue();
return (
-
-
+
+
{new Intl.NumberFormat("en", { style: "percent", notation: "compact", unitDisplay: "narrow" }).format(
progress,
)}
-
+
);
},
},
{
- ...columnsDefBase({ key: "ratio", showHeader: true, align: "center" }),
+ ...columnsDefBase({ key: "ratio", showHeader: true }),
sortUndefined: "last",
Cell: ({ cell }) => {
const ratio = cell.getValue();
- return ratio !== undefined && {ratio.toFixed(ratio >= 100 ? 0 : ratio >= 10 ? 1 : 2)};
+ return ratio !== undefined && {ratio.toFixed(ratio >= 100 ? 0 : ratio >= 10 ? 1 : 2)};
},
},
{
- ...columnsDefBase({ key: "received", showHeader: true, align: "right" }),
+ ...columnsDefBase({ key: "received", showHeader: true }),
Cell: ({ cell }) => {
const received = cell.getValue();
- return {humanFileSize(received)};
+ return {humanFileSize(received)};
},
},
{
- ...columnsDefBase({ key: "sent", showHeader: true, align: "right" }),
+ ...columnsDefBase({ key: "sent", showHeader: true }),
sortUndefined: "last",
Cell: ({ cell }) => {
const sent = cell.getValue();
- return sent && {humanFileSize(sent)};
+ return sent && {humanFileSize(sent)};
},
},
{
- ...columnsDefBase({ key: "size", showHeader: true, align: "right" }),
+ ...columnsDefBase({ key: "size", showHeader: true }),
Cell: ({ cell }) => {
const size = cell.getValue();
- return {humanFileSize(size)};
+ return {humanFileSize(size)};
},
},
{
@@ -557,25 +510,25 @@ export default function DownloadClientsWidget({
enableSorting: false,
Cell: ({ cell }) => {
const state = cell.getValue();
- return {t(`states.${state}`)};
+ return {t(`states.${state}`)};
},
},
{
- ...columnsDefBase({ key: "time", showHeader: true, align: "center" }),
+ ...columnsDefBase({ key: "time", showHeader: true }),
Cell: ({ cell }) => {
const time = cell.getValue();
- return time === 0 ? : {dayjs().add(time).fromNow()};
+ return time === 0 ? : {dayjs().add(time).fromNow()};
},
},
{
...columnsDefBase({ key: "type", showHeader: true }),
Cell: ({ cell }) => {
const type = cell.getValue();
- return {type};
+ return {type};
},
},
{
- ...columnsDefBase({ key: "upSpeed", showHeader: true, align: "right" }),
+ ...columnsDefBase({ key: "upSpeed", showHeader: true }),
sortUndefined: "last",
Cell: ({ cell }) => {
const upSpeed = cell.getValue();
@@ -604,17 +557,17 @@ export default function DownloadClientsWidget({
mantineTableContainerProps: { style: { height: "100%" } },
mantineTableProps: {
className: "downloads-widget-table",
- style: {
- "--sortButtonSize": "var(--button-size)",
- "--dragButtonSize": "var(--button-size)",
- },
},
mantineTableBodyProps: { style: editStyle },
+ mantineTableHeadCellProps: {
+ p: 4,
+ },
mantineTableBodyCellProps: ({ cell, row }) => ({
onClick: () => {
setClickedIndex(row.index);
if (cell.column.id !== "actions") open();
},
+ p: 4,
}),
onColumnOrderChange: (order) => {
//Order has a tendency to add the disabled column at the end of the the real ordered array
@@ -666,26 +619,25 @@ export default function DownloadClientsWidget({
if (options.columns.length === 0)
return (
- {t("errors.noColumns")}
+ {t("errors.noColumns")}
);
//The actual widget
return (
-
+
{integrationTypes.includes("torrent") && (
-
- {`${t("globalRatio")}:`}
- {(globalTraffic.up / globalTraffic.down).toFixed(2)}
+
+ {`${t("globalRatio")}:`}
+ {(globalTraffic.up / globalTraffic.down).toFixed(2)}
)}
-
-
+
+
-
+
{t("items.integration.columnTitle")}
-
+
{clients.map((client) => (
))}
@@ -847,37 +799,37 @@ const ClientsControl = ({ clients, filters, setFilters, availableStatuses }: Cli
{someInteract && (
mutateResumeQueue({ integrationIds: integrationsStatuses.paused })}
>
-
+
)}
{someInteract && (
mutatePauseQueue({ integrationIds: integrationsStatuses.active })}
>
-
+
)}
@@ -967,9 +919,8 @@ const ClientAvatar = ({ client }: ClientAvatarProps) => {
key={client.integration.id}
src={getIconUrl(client.integration.kind)}
style={{ filter: !isConnected ? "grayscale(100%)" : undefined }}
- size={30}
+ size="sm"
p={5}
- bd={`calc(var(--space-size)*0.5) solid ${client.status ? "transparent" : "var(--mantine-color-red-filled)"}`}
/>
);
};
diff --git a/packages/widgets/src/health-monitoring/cluster/cluster-health.tsx b/packages/widgets/src/health-monitoring/cluster/cluster-health.tsx
index c2abec2b8..db9718990 100644
--- a/packages/widgets/src/health-monitoring/cluster/cluster-health.tsx
+++ b/packages/widgets/src/health-monitoring/cluster/cluster-health.tsx
@@ -31,6 +31,7 @@ const running = (total: number, current: Resource) => {
export const ClusterHealthMonitoring = ({
integrationId,
options,
+ width,
}: WidgetComponentProps<"healthMonitoring"> & { integrationId: string }) => {
const t = useI18n();
const [healthData] = clientApi.widget.healthMonitoring.getClusterHealthStatus.useSuspenseQuery(
@@ -72,14 +73,15 @@ export const ClusterHealthMonitoring = ({
const cpuPercent = maxCpu ? (usedCpu / maxCpu) * 100 : 0;
const memPercent = maxMem ? (usedMem / maxMem) * 100 : 0;
+ const isTiny = width < 256;
return (
-
-
-
+
+
+
{formatUptime(uptime, t)}
-
+
-
+
-
+
-
+
-
+
@@ -140,45 +146,50 @@ export const ClusterHealthMonitoring = ({
interface SummaryHeaderProps {
cpu: number;
memory: number;
+ isTiny: boolean;
}
-const SummaryHeader = ({ cpu, memory }: SummaryHeaderProps) => {
+const SummaryHeader = ({ cpu, memory, isTiny }: SummaryHeaderProps) => {
const t = useI18n();
return (
-
+
-
+
}
sections={[{ value: cpu, color: cpu > 75 ? "orange" : "green" }]}
/>
- {t("widget.healthMonitoring.cluster.summary.cpu")}
- {cpu.toFixed(1)}%
+
+ {t("widget.healthMonitoring.cluster.summary.cpu")}
+
+ {cpu.toFixed(1)}%
-
+
}
sections={[{ value: memory, color: memory > 75 ? "orange" : "green" }]}
/>
- {t("widget.healthMonitoring.cluster.summary.memory")}
- {memory.toFixed(1)}%
+
+ {t("widget.healthMonitoring.cluster.summary.memory")}
+
+ {memory.toFixed(1)}%
diff --git a/packages/widgets/src/health-monitoring/cluster/resource-accordion-item.tsx b/packages/widgets/src/health-monitoring/cluster/resource-accordion-item.tsx
index 717822e60..5c6aa9dc4 100644
--- a/packages/widgets/src/health-monitoring/cluster/resource-accordion-item.tsx
+++ b/packages/widgets/src/health-monitoring/cluster/resource-accordion-item.tsx
@@ -13,6 +13,7 @@ interface ResourceAccordionItemProps {
activeCount: number;
totalCount: number;
};
+ isTiny: boolean;
}
export const ResourceAccordionItem = ({
@@ -21,13 +22,14 @@ export const ResourceAccordionItem = ({
icon: Icon,
badge,
children,
+ isTiny,
}: PropsWithChildren) => {
return (
- }>
-
- {title}
-
+ }>
+
+ {title}
+
{badge.activeCount} / {badge.totalCount}
diff --git a/packages/widgets/src/health-monitoring/cluster/resource-table.tsx b/packages/widgets/src/health-monitoring/cluster/resource-table.tsx
index bc8226f37..8f3637f19 100644
--- a/packages/widgets/src/health-monitoring/cluster/resource-table.tsx
+++ b/packages/widgets/src/health-monitoring/cluster/resource-table.tsx
@@ -1,4 +1,4 @@
-import { Group, Indicator, Popover, Table, Text } from "@mantine/core";
+import { Group, Indicator, Popover, Table, TableTbody, TableThead, TableTr, Text } from "@mantine/core";
import type { Resource } from "@homarr/integrations/types";
import { useI18n } from "@homarr/translation/client";
@@ -8,36 +8,47 @@ import { ResourcePopover } from "./resource-popover";
interface ResourceTableProps {
type: Resource["type"];
data: Resource[];
+ isTiny: boolean;
}
-export const ResourceTable = ({ type, data }: ResourceTableProps) => {
+export const ResourceTable = ({ type, data, isTiny }: ResourceTableProps) => {
const t = useI18n();
return (
-
-
- {t("widget.healthMonitoring.cluster.table.header.name")}
+
+
+
+ {t("widget.healthMonitoring.cluster.table.header.name")}
+
{type !== "storage" ? (
- {t("widget.healthMonitoring.cluster.table.header.cpu")}
+
+ {t("widget.healthMonitoring.cluster.table.header.cpu")}
+
) : null}
{type !== "storage" ? (
- {t("widget.healthMonitoring.cluster.table.header.memory")}
+
+ {t("widget.healthMonitoring.cluster.table.header.memory")}
+
) : null}
{type === "storage" ? (
- {t("widget.healthMonitoring.cluster.table.header.node")}
+
+ {t("widget.healthMonitoring.cluster.table.header.node")}
+
) : null}
-
-
-
+
+
+
{data.map((item) => {
return (
-
+
|
-
-
- {item.name}
+
+
+
+ {item.name}
+
|
{item.type === "storage" ? (
@@ -50,12 +61,12 @@ export const ResourceTable = ({ type, data }: ResourceTableProps) => {
>
)}
-
+
);
})}
-
+
);
};
diff --git a/packages/widgets/src/health-monitoring/component.tsx b/packages/widgets/src/health-monitoring/component.tsx
index 837e04504..d1ee3dd28 100644
--- a/packages/widgets/src/health-monitoring/component.tsx
+++ b/packages/widgets/src/health-monitoring/component.tsx
@@ -31,30 +31,20 @@ export default function HealthMonitoringWidget(props: WidgetComponentProps<"heal
}
return (
-
+
-
+
{t("widget.healthMonitoring.tab.system")}
-
+
{t("widget.healthMonitoring.tab.cluster")}
-
+
-
+
diff --git a/packages/widgets/src/health-monitoring/rings/cpu-ring.tsx b/packages/widgets/src/health-monitoring/rings/cpu-ring.tsx
index 0069c6d96..ff1746eea 100644
--- a/packages/widgets/src/health-monitoring/rings/cpu-ring.tsx
+++ b/packages/widgets/src/health-monitoring/rings/cpu-ring.tsx
@@ -1,33 +1,30 @@
-import { Box, Center, RingProgress, Text } from "@mantine/core";
-import { useElementSize } from "@mantine/hooks";
+import { Center, RingProgress, Text } from "@mantine/core";
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
-
+export const CpuRing = ({ cpuUtilization, isTiny }: { cpuUtilization: number; isTiny: boolean }) => {
return (
-
-
- {`${cpuUtilization.toFixed(2)}%`}
-
-
- }
- sections={[
- {
- value: Number(cpuUtilization.toFixed(2)),
- color: progressColor(Number(cpuUtilization.toFixed(2))),
- },
- ]}
- />
-
+
+ {`${cpuUtilization.toFixed(2)}%`}
+
+
+ }
+ sections={[
+ {
+ value: Number(cpuUtilization.toFixed(2)),
+ color: progressColor(Number(cpuUtilization.toFixed(2))),
+ },
+ ]}
+ />
);
};
diff --git a/packages/widgets/src/health-monitoring/rings/cpu-temp-ring.tsx b/packages/widgets/src/health-monitoring/rings/cpu-temp-ring.tsx
index a70f26a90..2c6a2b254 100644
--- a/packages/widgets/src/health-monitoring/rings/cpu-temp-ring.tsx
+++ b/packages/widgets/src/health-monitoring/rings/cpu-temp-ring.tsx
@@ -1,39 +1,41 @@
-import { Box, Center, RingProgress, Text } from "@mantine/core";
-import { useElementSize } from "@mantine/hooks";
+import { Center, RingProgress, Text } from "@mantine/core";
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
-
+export const CpuTempRing = ({
+ fahrenheit,
+ cpuTemp,
+ isTiny,
+}: {
+ fahrenheit: boolean;
+ cpuTemp: number | undefined;
+ isTiny: boolean;
+}) => {
if (!cpuTemp) {
return null;
}
return (
-
-
-
- {fahrenheit ? `${(cpuTemp * 1.8 + 32).toFixed(1)}°F` : `${cpuTemp.toFixed(1)}°C`}
-
-
-
- }
- sections={[
- {
- value: cpuTemp,
- color: progressColor(cpuTemp),
- },
- ]}
- />
-
+
+
+ {fahrenheit ? `${(cpuTemp * 1.8 + 32).toFixed(1)}°F` : `${cpuTemp.toFixed(1)}°C`}
+
+
+
+ }
+ sections={[
+ {
+ value: cpuTemp,
+ color: progressColor(cpuTemp),
+ },
+ ]}
+ />
);
};
diff --git a/packages/widgets/src/health-monitoring/rings/memory-ring.tsx b/packages/widgets/src/health-monitoring/rings/memory-ring.tsx
index 2d8ce4d68..e19ad0fe6 100644
--- a/packages/widgets/src/health-monitoring/rings/memory-ring.tsx
+++ b/packages/widgets/src/health-monitoring/rings/memory-ring.tsx
@@ -1,38 +1,33 @@
-import { Box, Center, RingProgress, Text } from "@mantine/core";
-import { useElementSize } from "@mantine/hooks";
+import { Center, RingProgress, Text } from "@mantine/core";
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
+export const MemoryRing = ({ available, used, isTiny }: { available: string; used: string; isTiny: boolean }) => {
const memoryUsage = formatMemoryUsage(available, used);
return (
-
-
-
- {memoryUsage.memUsed.GB}GiB
-
-
-
- }
- sections={[
- {
- value: Number(memoryUsage.memUsed.percent),
- color: progressColor(Number(memoryUsage.memUsed.percent)),
- tooltip: `${memoryUsage.memUsed.percent}%`,
- },
- ]}
- />
-
+
+
+ {memoryUsage.memUsed.GB}GiB
+
+
+
+ }
+ sections={[
+ {
+ value: Number(memoryUsage.memUsed.percent),
+ color: progressColor(Number(memoryUsage.memUsed.percent)),
+ tooltip: `${memoryUsage.memUsed.percent}%`,
+ },
+ ]}
+ />
);
};
diff --git a/packages/widgets/src/health-monitoring/system-health.tsx b/packages/widgets/src/health-monitoring/system-health.tsx
index 82109f87a..6e4aa2ed4 100644
--- a/packages/widgets/src/health-monitoring/system-health.tsx
+++ b/packages/widgets/src/health-monitoring/system-health.tsx
@@ -44,7 +44,11 @@ import classes from "./system-health.module.css";
dayjs.extend(duration);
-export const SystemHealthMonitoring = ({ options, integrationIds }: WidgetComponentProps<"healthMonitoring">) => {
+export const SystemHealthMonitoring = ({
+ options,
+ integrationIds,
+ width,
+}: WidgetComponentProps<"healthMonitoring">) => {
const t = useI18n();
const [healthData] = clientApi.widget.healthMonitoring.getSystemHealthStatus.useSuspenseQuery(
{
@@ -79,6 +83,8 @@ export const SystemHealthMonitoring = ({ options, integrationIds }: WidgetCompon
},
);
+ const isTiny = width < 256;
+
return (
{healthData.map(({ integrationId, integrationName, healthInfo, updatedAt }) => {
@@ -91,95 +97,93 @@ export const SystemHealthMonitoring = ({ options, integrationIds }: WidgetCompon
h="100%"
className={`health-monitoring-information health-monitoring-${integrationName}`}
p="sm"
+ pos="relative"
>
-
-
+ 0 ? "blue" : "gray"}
+ position="top-end"
+ size={16}
+ label={healthInfo.availablePkgUpdates > 0 ? healthInfo.availablePkgUpdates : undefined}
+ disabled={!healthInfo.rebootRequired && healthInfo.availablePkgUpdates === 0}
>
-
- 0 ? "blue" : "gray"}
- position="top-end"
- size="md"
- label={healthInfo.availablePkgUpdates > 0 ? healthInfo.availablePkgUpdates : undefined}
- disabled={!healthInfo.rebootRequired && healthInfo.availablePkgUpdates === 0}
- >
-
-
-
-
-
-
-
-
- }>
- {t("widget.healthMonitoring.popover.processor", { cpuModelName: healthInfo.cpuModelName })}
-
- }>
- {t("widget.healthMonitoring.popover.memory", { memory: memoryUsage.memTotal.GB })}
-
- }>
- {t("widget.healthMonitoring.popover.memoryAvailable", {
- memoryAvailable: memoryUsage.memFree.GB,
- percent: memoryUsage.memFree.percent,
- })}
-
- }>
- {t("widget.healthMonitoring.popover.version", {
- version: healthInfo.version,
- })}
-
- }>
- {formatUptime(healthInfo.uptime, t)}
-
- }>
- {t("widget.healthMonitoring.popover.loadAverage")}
-
-
}>
-
- {t("widget.healthMonitoring.popover.minute")} {healthInfo.loadAverage["1min"]}%
-
-
- {t("widget.healthMonitoring.popover.minutes", { count: 5 })}{" "}
- {healthInfo.loadAverage["5min"]}%
-
-
- {t("widget.healthMonitoring.popover.minutes", { count: 15 })}{" "}
- {healthInfo.loadAverage["15min"]}%
-
-
-
-
-
-
- {options.cpu && }
- {options.cpu && }
- {options.memory && }
-
- {
-
- {t("widget.healthMonitoring.popover.lastSeen", { lastSeen: dayjs(updatedAt).fromNow() })}
-
- }
+
+
+
+
+
+
+
+
+ }>
+ {t("widget.healthMonitoring.popover.processor", { cpuModelName: healthInfo.cpuModelName })}
+
+ }>
+ {t("widget.healthMonitoring.popover.memory", { memory: memoryUsage.memTotal.GB })}
+
+ }>
+ {t("widget.healthMonitoring.popover.memoryAvailable", {
+ memoryAvailable: memoryUsage.memFree.GB,
+ percent: String(memoryUsage.memFree.percent),
+ })}
+
+ }>
+ {t("widget.healthMonitoring.popover.version", {
+ version: healthInfo.version,
+ })}
+
+ }>
+ {formatUptime(healthInfo.uptime, t)}
+
+ }>
+ {t("widget.healthMonitoring.popover.loadAverage")}
+
+
}>
+
+ {t("widget.healthMonitoring.popover.minute")} {healthInfo.loadAverage["1min"]}%
+
+
+ {t("widget.healthMonitoring.popover.minutes", { count: "5" })} {healthInfo.loadAverage["5min"]}%
+
+
+ {t("widget.healthMonitoring.popover.minutes", { count: "15" })}{" "}
+ {healthInfo.loadAverage["15min"]}%
+
+
+
+
+
+
+ {options.cpu && }
+ {options.cpu && (
+
+ )}
+ {options.memory && (
+
+ )}
+
+ {
+
+ {t("widget.healthMonitoring.popover.lastSeen", { lastSeen: dayjs(updatedAt).fromNow() })}
+
+ }
{options.fileSystem &&
disksData.map((disk) => {
return (
@@ -188,63 +192,72 @@ export const SystemHealthMonitoring = ({ options, integrationIds }: WidgetCompon
`health-monitoring-disk-card health-monitoring-disk-card-${integrationName}`,
classes.card,
)}
+ style={{ overflow: "visible" }}
key={disk.deviceName}
radius={board.itemRadius}
- p="sm"
+ p="xs"
>
-
-
-
-
- {disk.deviceName}
-
-
-
-
-
- {options.fahrenheit
- ? `${(disk.temperature * 1.8 + 32).toFixed(1)}°F`
- : `${disk.temperature}°C`}
-
-
-
-
-
- {disk.overallStatus ? disk.overallStatus : "N/A"}
-
-
-
-
-
-
-
- {t("widget.healthMonitoring.popover.used")}
-
-
-
-
- = 1
- ? `${(Number(disk.available) / 1024 ** 4).toFixed(2)} TiB`
- : `${(Number(disk.available) / 1024 ** 3).toFixed(2)} GiB`
- }
+
+
-
+
+
+ {disk.deviceName}
+
+
+
+
+
+ {options.fahrenheit
+ ? `${(disk.temperature * 1.8 + 32).toFixed(1)}°F`
+ : `${disk.temperature}°C`}
+
+
+
+
+
+ {disk.overallStatus ? disk.overallStatus : "N/A"}
+
+
+
+
+
+
+
+ {t("widget.healthMonitoring.popover.used")}
+
+
+
+
+ = 1
+ ? `${(Number(disk.available) / 1024 ** 4).toFixed(2)} TiB`
+ : `${(Number(disk.available) / 1024 ** 3).toFixed(2)} GiB`
+ }
>
-
- {t("widget.healthMonitoring.popover.available")}
-
-
-
-
+
+
+ {t("widget.healthMonitoring.popover.available")}
+
+
+
+
+
);
})}
@@ -262,7 +275,12 @@ export const formatUptime = (uptimeInSeconds: number, t: TranslationFunction) =>
const hours = uptimeDuration.hours();
const minutes = uptimeDuration.minutes();
- return t("widget.healthMonitoring.popover.uptime", { months, days, hours, minutes });
+ return t("widget.healthMonitoring.popover.uptime", {
+ months: String(months),
+ days: String(days),
+ hours: String(hours),
+ minutes: String(minutes),
+ });
};
export const progressColor = (percentage: number) => {
diff --git a/packages/widgets/src/indexer-manager/component.tsx b/packages/widgets/src/indexer-manager/component.tsx
index 12aed440a..e406ffa8a 100644
--- a/packages/widgets/src/indexer-manager/component.tsx
+++ b/packages/widgets/src/indexer-manager/component.tsx
@@ -1,6 +1,6 @@
"use client";
-import { Anchor, Button, Card, Container, Flex, Group, ScrollArea, Text } from "@mantine/core";
+import { ActionIcon, Anchor, Button, Card, Flex, Group, ScrollArea, Stack, Text } from "@mantine/core";
import { IconCircleCheck, IconCircleX, IconReportSearch, IconTestPipe } from "@tabler/icons-react";
import combineClasses from "clsx";
@@ -11,7 +11,12 @@ import { useI18n } from "@homarr/translation/client";
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,
+ width,
+ height,
+}: WidgetComponentProps<"indexerManager">) {
const t = useI18n();
const [indexersData] = clientApi.widget.indexerManager.getIndexersStatus.useSuspenseQuery(
{ integrationIds },
@@ -40,37 +45,60 @@ export default function IndexerManagerWidget({ options, integrationIds }: Widget
},
);
+ const hasSmallWidth = width < 256;
+ const hasSmallHeight = height < 256;
+
return (
-
-
-
+
+
+
{t("widget.indexerManager.title")}
-
+ {hasSmallHeight && (
+ {
+ testAll({ integrationIds });
+ }}
+ >
+
+
+ )}
+
-
+
{indexersData.map(({ integrationId, indexers }) => (
-
+
{indexers.map((indexer) => (
-
+
{indexer.name}
@@ -78,35 +106,38 @@ export default function IndexerManagerWidget({ options, integrationIds }: Widget
) : (
)}
))}
-
+
))}
- }
- loading={isPending}
- loaderProps={{ type: "dots" }}
- onClick={() => {
- testAll({ integrationIds });
- }}
- >
- {t("widget.indexerManager.testAll")}
-
+ {!hasSmallHeight && (
+ }
+ loading={isPending}
+ loaderProps={{ type: "dots" }}
+ onClick={() => {
+ testAll({ integrationIds });
+ }}
+ >
+ {t("widget.indexerManager.testAll")}
+
+ )}
);
}
diff --git a/packages/widgets/src/media-requests/list/component.tsx b/packages/widgets/src/media-requests/list/component.tsx
index 07248d203..58499ecd4 100644
--- a/packages/widgets/src/media-requests/list/component.tsx
+++ b/packages/widgets/src/media-requests/list/component.tsx
@@ -16,6 +16,7 @@ export default function MediaServerWidget({
integrationIds,
isEditMode,
options,
+ width,
}: WidgetComponentProps<"mediaRequests-requestList">) {
const t = useScopedI18n("widget.mediaRequests-requestList");
const [mediaRequests] = clientApi.widget.mediaRequests.getLatestRequests.useSuspenseQuery(
@@ -59,19 +60,21 @@ export default function MediaServerWidget({
if (mediaRequests.length === 0) throw new NoIntegrationDataError();
+ const isTiny = width < 256;
+
return (
-
+
{mediaRequests.map((mediaRequest) => (
-
-
-
-
- {mediaRequest.airDate?.getFullYear() ?? t("toBeDetermined")}
-
-
- {getAvailabilityProperties(mediaRequest.availability, t).label}
-
+ {!isTiny && (
+
+ )}
+
+
+
+
+
+ {mediaRequest.airDate?.getFullYear() ?? t("toBeDetermined")}
+
+ {!isTiny && (
+
+ {getAvailabilityProperties(mediaRequest.availability, t).label}
+
+ )}
+
+
+
+
+ {(mediaRequest.requestedBy?.displayName ?? "") || "unknown"}
+
+
+
+
+
+ {mediaRequest.name || "unknown"}
+
+ {mediaRequest.status === MediaRequestStatus.PendingApproval ? (
+
+
+ {
+ mutateRequestAnswer({
+ integrationId: mediaRequest.integrationId,
+ requestId: mediaRequest.id,
+ answer: "approve",
+ });
+ }}
+ >
+
+
+
+
+ {
+ mutateRequestAnswer({
+ integrationId: mediaRequest.integrationId,
+ requestId: mediaRequest.id,
+ answer: "decline",
+ });
+ }}
+ >
+
+
+
+
+ ) : (
+
+ )}
-
- {mediaRequest.name || "unknown"}
-
-
-
-
-
- {(mediaRequest.requestedBy?.displayName ?? "") || "unknown"}
-
-
- {mediaRequest.status === MediaRequestStatus.PendingApproval ? (
-
-
- {
- mutateRequestAnswer({
- integrationId: mediaRequest.integrationId,
- requestId: mediaRequest.id,
- answer: "approve",
- });
- }}
- >
-
-
-
-
- {
- mutateRequestAnswer({
- integrationId: mediaRequest.integrationId,
- requestId: mediaRequest.id,
- answer: "decline",
- });
- }}
- >
-
-
-
-
- ) : (
-
- )}
-
))}
diff --git a/packages/widgets/src/media-requests/stats/component.tsx b/packages/widgets/src/media-requests/stats/component.tsx
index 59be66653..87e7da8aa 100644
--- a/packages/widgets/src/media-requests/stats/component.tsx
+++ b/packages/widgets/src/media-requests/stats/component.tsx
@@ -1,11 +1,9 @@
"use client";
-import { ActionIcon, Avatar, Card, Grid, Group, Space, Stack, Text, Tooltip } from "@mantine/core";
-import { useElementSize } from "@mantine/hooks";
+import { Avatar, Card, Grid, Group, Stack, Text, Tooltip } from "@mantine/core";
import type { Icon } from "@tabler/icons-react";
import {
IconDeviceTv,
- IconExternalLink,
IconHourglass,
IconLoaderQuarter,
IconMovie,
@@ -28,6 +26,7 @@ import classes from "./component.module.css";
export default function MediaServerWidget({
integrationIds,
isEditMode,
+ width,
}: WidgetComponentProps<"mediaRequests-requestStats">) {
const t = useScopedI18n("widget.mediaRequests-requestStats");
const [requestStats] = clientApi.widget.mediaRequests.getStats.useSuspenseQuery(
@@ -41,8 +40,6 @@ export default function MediaServerWidget({
},
);
- const { width, height, ref } = useElementSize();
-
const board = useRequiredBoard();
if (requestStats.users.length === 0 && requestStats.stats.length === 0) throw new NoIntegrationDataError();
@@ -90,94 +87,84 @@ export default function MediaServerWidget({
},
] satisfies { name: keyof RequestStats; icon: Icon; number: number }[];
+ const isTiny = width < 256;
+
return (
-
- {t("titles.stats.main")}
-
-
- {data.map((stat) => (
-
-
-
-
-
-
- {stat.number}
-
-
-
-
-
- ))}
-
-
- {t("titles.users.main")}
-
-
- {requestStats.users.slice(0, Math.max(Math.floor((height / width) * 5), 1)).map((user) => (
-
-
-
-
+
+
+ {t("titles.stats.main")}
+
+
+ {data.map((stat) => (
+
+
+
+
+
+
+ {stat.number}
+
+
+
-
-
- {user.displayName}
+
+ ))}
+
+
+
+
+ {t("titles.users.main")} ({t("titles.users.requests")})
+
+
+ {requestStats.users.slice(0, 10).map((user) => (
+
+
+
+
+
+
+
+ {user.displayName}
+
+
+
+
+ {user.requestCount}
-
- {`${t("titles.users.requests")}: ${user.requestCount}`}
-
-
-
-
-
-
-
-
- ))}
+
+
+ ))}
+
);
diff --git a/packages/widgets/src/media-server/component.tsx b/packages/widgets/src/media-server/component.tsx
index a52c0ee8f..146f7915f 100644
--- a/packages/widgets/src/media-server/component.tsx
+++ b/packages/widgets/src/media-server/component.tsx
@@ -1,8 +1,9 @@
"use client";
+import type { ReactNode } from "react";
import { useMemo } from "react";
-import { Avatar, Box, Flex, Group, Stack, Text, Title } from "@mantine/core";
-import { IconDeviceAudioTape, IconDeviceTv, IconMovie, IconVideo } from "@tabler/icons-react";
+import { Avatar, Flex, Group, Stack, Text, Title } from "@mantine/core";
+import { IconDeviceTv, IconHeadphones, IconMovie, IconVideo } from "@tabler/icons-react";
import type { MRT_ColumnDef } from "mantine-react-table";
import { MantineReactTable } from "mantine-react-table";
@@ -11,6 +12,7 @@ import { getIconUrl, integrationDefs } from "@homarr/definitions";
import type { StreamSession } from "@homarr/integrations";
import { createModal, useModalAction } from "@homarr/modals";
import { useScopedI18n } from "@homarr/translation/client";
+import type { TablerIcon } from "@homarr/ui";
import { useTranslatedMantineReactTable } from "@homarr/ui/hooks";
import type { WidgetComponentProps } from "../definition";
@@ -28,59 +30,51 @@ export default function MediaServerWidget({ integrationIds, isEditMode }: Widget
);
const utils = clientApi.useUtils();
+ const t = useScopedI18n("widget.mediaServer");
const columns = useMemo[]>(
() => [
{
accessorKey: "sessionName",
- header: "Name",
- mantineTableHeadCellProps: {
- style: {
- width: "30%",
- },
- },
+ header: t("items.name"),
+
Cell: ({ row }) => (
-
+
{row.original.sessionName}
),
},
{
accessorKey: "user.username",
- header: "User",
- mantineTableHeadCellProps: {
- style: {
- width: "25%",
- },
- },
+ header: t("items.user"),
+
Cell: ({ row }) => (
-
-
- {row.original.user.username}
+
+
+ {row.original.user.username}
),
},
{
accessorKey: "currentlyPlaying", // currentlyPlaying.name can be undefined which results in a warning. This is why we use currentlyPlaying instead of currentlyPlaying.name
- header: "Currently playing",
- mantineTableHeadCellProps: {
- style: {
- width: "45%",
- },
- },
- Cell: ({ row }) => {
- if (row.original.currentlyPlaying) {
- return (
-
- {row.original.currentlyPlaying.name}
-
- );
- }
+ header: t("items.currentlyPlaying"),
- return null;
+ Cell: ({ row }) => {
+ if (!row.original.currentlyPlaying) return null;
+
+ const Icon = mediaTypeIconMap[row.original.currentlyPlaying.type];
+
+ return (
+
+
+
+ {row.original.currentlyPlaying.name}
+
+
+ );
},
},
],
- [],
+ [t],
);
clientApi.widget.mediaServer.subscribeToCurrentStreams.useSubscription(
@@ -137,8 +131,18 @@ export default function MediaServerWidget({ integrationIds, isEditMode }: Widget
enableDensityToggle: false,
enableFilters: false,
enableHiding: false,
+ enableColumnPinning: true,
initialState: {
density: "xs",
+ columnPinning: {
+ right: ["currentlyPlaying"],
+ },
+ },
+ mantineTableHeadProps: {
+ fz: "xs",
+ },
+ mantineTableHeadCellProps: {
+ py: 4,
},
mantinePaperProps: {
flex: 1,
@@ -158,20 +162,16 @@ export default function MediaServerWidget({ integrationIds, isEditMode }: Widget
},
mantineTableBodyCellProps: ({ row }) => ({
onClick: () => {
- openModal({
- item: row.original,
- title:
- row.original.currentlyPlaying?.type === "movie" ? (
-
- ) : row.original.currentlyPlaying?.type === "tv" ? (
-
- ) : row.original.currentlyPlaying?.type === "video" ? (
-
- ) : (
-
- ),
- });
+ openModal(
+ {
+ item: row.original,
+ },
+ {
+ title: row.original.sessionName,
+ },
+ );
},
+ py: 4,
}),
});
@@ -210,42 +210,64 @@ export default function MediaServerWidget({ integrationIds, isEditMode }: Widget
);
}
-const itemInfoModal = createModal<{ item: StreamSession; title: React.ReactNode }>(({ innerProps }) => {
+const itemInfoModal = createModal<{ item: StreamSession }>(({ innerProps }) => {
const t = useScopedI18n("widget.mediaServer.items");
+ const Icon = innerProps.item.currentlyPlaying ? mediaTypeIconMap[innerProps.item.currentlyPlaying.type] : null;
return (
- {innerProps.title}
- {innerProps.item.currentlyPlaying?.name}
-
- {innerProps.item.currentlyPlaying?.episodeName}
- {innerProps.item.currentlyPlaying?.seasonName && (
- <>
- {" - "}
- {innerProps.item.currentlyPlaying.seasonName}
- >
- )}
-
+ {Icon && innerProps.item.currentlyPlaying !== null && (
+
+
+ {innerProps.item.currentlyPlaying.name}
+
+ )}
+ {innerProps.item.currentlyPlaying?.episodeName && (
+
+ {innerProps.item.currentlyPlaying.episodeName}
+ {innerProps.item.currentlyPlaying.seasonName && (
+ <>
+ {" - "}
+ {innerProps.item.currentlyPlaying.seasonName}
+ >
+ )}
+
+ )}
-
-
-
+
+ {" "}
+ {innerProps.item.user.username}
+
+ }
+ />
+ {innerProps.item.sessionName}} />
+ {innerProps.item.sessionId}} />
);
}).withOptions({
defaultTitle() {
return "";
},
- size: "auto",
+ size: "lg",
centered: true,
});
-const NormalizedLine = ({ itemKey, value }: { itemKey: string; value: string }) => {
+const NormalizedLine = ({ itemKey, value }: { itemKey: string; value: ReactNode }) => {
return (
{itemKey}:
- {value}
+ {value}
);
};
+
+const mediaTypeIconMap = {
+ movie: IconMovie,
+ tv: IconDeviceTv,
+ video: IconVideo,
+ audio: IconHeadphones,
+} satisfies Record["type"], TablerIcon>;
diff --git a/packages/widgets/src/media-transcoding/component.tsx b/packages/widgets/src/media-transcoding/component.tsx
index 10342ec6a..d66da98e6 100644
--- a/packages/widgets/src/media-transcoding/component.tsx
+++ b/packages/widgets/src/media-transcoding/component.tsx
@@ -2,20 +2,32 @@
import { useState } from "react";
import { Center, Divider, Group, Pagination, SegmentedControl, Stack, Text } from "@mantine/core";
+import type { TablerIcon } from "@tabler/icons-react";
import { IconClipboardList, IconCpu2, IconReportAnalytics } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
import { useI18n } from "@homarr/translation/client";
+import { views } from ".";
import type { WidgetComponentProps } from "../definition";
import { HealthCheckStatus } from "./health-check-status";
import { QueuePanel } from "./panels/queue.panel";
import { StatisticsPanel } from "./panels/statistics.panel";
import { WorkersPanel } from "./panels/workers.panel";
-type Views = "workers" | "queue" | "statistics";
+type View = (typeof views)[number];
-export default function MediaTranscodingWidget({ integrationIds, options }: WidgetComponentProps<"mediaTranscoding">) {
+const viewIcons = {
+ workers: IconCpu2,
+ queue: IconClipboardList,
+ statistics: IconReportAnalytics,
+} satisfies Record;
+
+export default function MediaTranscodingWidget({
+ integrationIds,
+ options,
+ width,
+}: WidgetComponentProps<"mediaTranscoding">) {
const [queuePage, setQueuePage] = useState(1);
const queuePageSize = 10;
const [transcodingData] = clientApi.widget.mediaTranscoding.getDataAsync.useSuspenseQuery(
@@ -31,15 +43,16 @@ export default function MediaTranscodingWidget({ integrationIds, options }: Widg
},
);
- const [view, setView] = useState(options.defaultView);
+ const [view, setView] = useState(options.defaultView);
const totalQueuePages = Math.ceil((transcodingData.data.queue.totalCount || 1) / queuePageSize);
const t = useI18n("widget.mediaTranscoding");
+ const isTiny = width < 256;
return (
{view === "workers" ? (
-
+
) : view === "queue" ? (
) : (
@@ -48,65 +61,48 @@ export default function MediaTranscodingWidget({ integrationIds, options }: Widg
{
+ const Icon = viewIcons[value];
+ return {
label: (
-
-
-
- {t("tab.workers")}
-
+
+
+ {!isTiny && (
+
+ {t(`tab.${value}`)}
+
+ )}
),
- value: "workers",
- },
- {
- label: (
-
-
-
- {t("tab.queue")}
-
-
- ),
- value: "queue",
- },
- {
- label: (
-
-
-
- {t("tab.statistics")}
-
-
- ),
- value: "statistics",
- },
- ]}
+ value,
+ };
+ })}
value={view}
- onChange={(value) => setView(value as Views)}
+ onChange={(value) => setView(value as View)}
size="xs"
/>
- {view === "queue" && (
- <>
-
-
-
-
-
-
-
-
-
- {t("currentIndex", {
- start: transcodingData.data.queue.startIndex + 1,
- end: transcodingData.data.queue.endIndex + 1,
- total: transcodingData.data.queue.totalCount,
- })}
-
- >
- )}
+
+ {view === "queue" && (
+ <>
+
+
+ {!isTiny && }
+
+
+ {!isTiny && }
+
+
+
+ {t("currentIndex", {
+ start: String(transcodingData.data.queue.startIndex + 1),
+ end: String(transcodingData.data.queue.endIndex + 1),
+ total: String(transcodingData.data.queue.totalCount),
+ })}
+
+ >
+ )}
+
diff --git a/packages/widgets/src/media-transcoding/health-check-status.tsx b/packages/widgets/src/media-transcoding/health-check-status.tsx
index 6491f6289..d1b3f7df9 100644
--- a/packages/widgets/src/media-transcoding/health-check-status.tsx
+++ b/packages/widgets/src/media-transcoding/health-check-status.tsx
@@ -23,8 +23,8 @@ export function HealthCheckStatus(props: HealthCheckStatusProps) {
return (
-
-
+
+
diff --git a/packages/widgets/src/media-transcoding/index.ts b/packages/widgets/src/media-transcoding/index.ts
index 355f3d9d3..0c6239eea 100644
--- a/packages/widgets/src/media-transcoding/index.ts
+++ b/packages/widgets/src/media-transcoding/index.ts
@@ -1,20 +1,20 @@
import { IconTransform } from "@tabler/icons-react";
import { z } from "zod";
+import { capitalize } from "@homarr/common";
+
import { createWidgetDefinition } from "../definition";
import { optionsBuilder } from "../options";
+export const views = ["workers", "queue", "statistics"] as const;
+
export const { componentLoader, definition } = createWidgetDefinition("mediaTranscoding", {
icon: IconTransform,
createOptions() {
return optionsBuilder.from((factory) => ({
defaultView: factory.select({
defaultValue: "statistics",
- options: [
- { label: "Workers", value: "workers" },
- { label: "Queue", value: "queue" },
- { label: "Statistics", value: "statistics" },
- ],
+ options: views.map((view) => ({ label: capitalize(view), value: view })),
}),
queuePageSize: factory.number({ defaultValue: 10, validate: z.number().min(1).max(30) }),
}));
diff --git a/packages/widgets/src/media-transcoding/panels/queue.panel.tsx b/packages/widgets/src/media-transcoding/panels/queue.panel.tsx
index 9d4d6a680..45cc7581f 100644
--- a/packages/widgets/src/media-transcoding/panels/queue.panel.tsx
+++ b/packages/widgets/src/media-transcoding/panels/queue.panel.tsx
@@ -1,4 +1,4 @@
-import { Center, Group, ScrollArea, Table, Text, Title, Tooltip } from "@mantine/core";
+import { Center, Group, ScrollArea, Table, TableTd, TableTh, TableTr, Text, Title, Tooltip } from "@mantine/core";
import { IconHeartbeat, IconTransform } from "@tabler/icons-react";
import { humanFileSize } from "@homarr/common";
@@ -17,7 +17,7 @@ export function QueuePanel(props: QueuePanelProps) {
if (queue.array.length === 0) {
return (
- {t("empty")}
+ {t("empty")}
);
}
@@ -26,36 +26,42 @@ export function QueuePanel(props: QueuePanelProps) {
-
- | {t("table.file")} |
- {t("table.size")} |
-
+
+
+
+ {t("table.file")}
+
+
+
+
+ {t("table.size")}
+
+
+
{queue.array.map((item) => (
-
- |
-
-
- {item.type === "transcode" ? (
-
-
-
- ) : (
-
-
-
- )}
-
+
+
+
+ {item.type === "transcode" ? (
+
+
+
+ ) : (
+
+
+
+ )}
{item.filePath.split("\\").pop()?.split("/").pop() ?? item.filePath}
- |
-
+
+
{humanFileSize(item.fileSize)}
- |
-
+
+
))}
diff --git a/packages/widgets/src/media-transcoding/panels/statistics.panel.tsx b/packages/widgets/src/media-transcoding/panels/statistics.panel.tsx
index 93da0c0ce..23ab97563 100644
--- a/packages/widgets/src/media-transcoding/panels/statistics.panel.tsx
+++ b/packages/widgets/src/media-transcoding/panels/statistics.panel.tsx
@@ -1,11 +1,12 @@
-import type react from "react";
import type { MantineColor, RingProgressProps } from "@mantine/core";
-import { Box, Center, Grid, Group, RingProgress, Stack, Text, Title, useMantineColorScheme } from "@mantine/core";
+import { Card, Center, Group, RingProgress, ScrollArea, Stack, Text, Title, Tooltip } from "@mantine/core";
import { IconDatabaseHeart, IconFileDescription, IconHeartbeat, IconTransform } from "@tabler/icons-react";
+import { useRequiredBoard } from "@homarr/boards/context";
import { humanFileSize } from "@homarr/common";
import type { TdarrPieSegment, TdarrStatistics } from "@homarr/integrations";
import { useI18n } from "@homarr/translation/client";
+import type { TablerIcon } from "@homarr/ui";
const PIE_COLORS: MantineColor[] = ["cyan", "grape", "gray", "orange", "pink"];
@@ -21,90 +22,54 @@ export function StatisticsPanel(props: StatisticsPanelProps) {
if (!allLibs) {
return (
- {t("empty")}
+ {t("empty")}
);
}
return (
-
-
-
-
- {t("transcodes")}
-
-
-
- }
- label={t("transcodesCount", {
- value: props.statistics.totalTranscodeCount,
- })}
- />
-
-
- }
- label={t("healthChecksCount", {
- value: props.statistics.totalHealthCheckCount,
- })}
- />
-
-
- }
- label={t("filesCount", {
- value: props.statistics.totalFileCount,
- })}
- />
-
-
- }
- label={t("savedSpace", {
- value: humanFileSize(Math.floor(allLibs.savedSpace)),
- })}
- />
-
-
-
-
- {t("healthChecks")}
-
+
+
+
+
+
+
-
-
-
- {t("videoCodecs")}
-
-
-
- {t("videoContainers")}
-
-
-
- {t("videoResolutions")}
-
+
+
+
+
+
+
-
+
);
}
+interface StatisticRingProgressProps {
+ items: TdarrPieSegment[];
+ label: string;
+}
+
+const StatisticRingProgress = ({ items, label }: StatisticRingProgressProps) => {
+ return (
+
+
+ {label}
+
+
+
+ );
+};
+
function toRingProgressSections(segments: TdarrPieSegment[]): RingProgressProps["sections"] {
const total = segments.reduce((prev, curr) => prev + curr.value, 0);
return segments.map((segment, index) => ({
@@ -115,26 +80,22 @@ function toRingProgressSections(segments: TdarrPieSegment[]): RingProgressProps[
}));
}
-interface StatBoxProps {
- icon: react.ReactNode;
+interface StatisticItemProps {
+ icon: TablerIcon;
+ value: string | number;
label: string;
}
-function StatBox(props: StatBoxProps) {
- const { colorScheme } = useMantineColorScheme();
+function StatisticItem(props: StatisticItemProps) {
+ const board = useRequiredBoard();
return (
- ({
- padding: theme.spacing.xs,
- border: "1px solid",
- borderRadius: theme.radius.md,
- borderColor: colorScheme === "dark" ? theme.colors.dark[5] : theme.colors.gray[1],
- })}
- >
-
- {props.icon}
- {props.label}
-
-
+
+
+
+
+ {props.value}
+
+
+
);
}
diff --git a/packages/widgets/src/media-transcoding/panels/workers.panel.tsx b/packages/widgets/src/media-transcoding/panels/workers.panel.tsx
index 961ac5d7c..d1602c248 100644
--- a/packages/widgets/src/media-transcoding/panels/workers.panel.tsx
+++ b/packages/widgets/src/media-transcoding/panels/workers.panel.tsx
@@ -1,4 +1,16 @@
-import { Center, Group, Progress, ScrollArea, Table, Text, Title, Tooltip } from "@mantine/core";
+import {
+ Center,
+ Group,
+ Progress,
+ ScrollArea,
+ Table,
+ TableTd,
+ TableTh,
+ TableTr,
+ Text,
+ Title,
+ Tooltip,
+} from "@mantine/core";
import { IconHeartbeat, IconTransform } from "@tabler/icons-react";
import type { TdarrWorker } from "@homarr/integrations";
@@ -6,6 +18,7 @@ import { useI18n } from "@homarr/translation/client";
interface WorkersPanelProps {
workers: TdarrWorker[];
+ isTiny: boolean;
}
export function WorkersPanel(props: WorkersPanelProps) {
@@ -14,7 +27,7 @@ export function WorkersPanel(props: WorkersPanelProps) {
if (props.workers.length === 0) {
return (
- {t("empty")}
+ {t("empty")}
);
}
@@ -23,52 +36,71 @@ export function WorkersPanel(props: WorkersPanelProps) {
-
- | {t("table.file")} |
- {t("table.eta")} |
- {t("table.progress")} |
-
+
+
+
+ {t("table.file")}
+
+
+
+
+ {t("table.eta")}
+
+
+
+
+ {t("table.progress")}
+
+
+
- {props.workers.map((worker) => (
-
-
-
-
- {worker.jobType === "transcode" ? (
-
-
-
- ) : (
-
-
-
+ {props.workers.map((worker) => {
+ const fileName = worker.filePath.split("\\").pop()?.split("/").pop() ?? worker.filePath;
+ return (
+
+
+
+
+ {worker.jobType === "transcode" ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+ {fileName}
+
+
+
+
+ {worker.ETA.startsWith("0:") ? worker.ETA.substring(2) : worker.ETA}
+
+
+
+ {!props.isTiny && (
+ <>
+ {worker.step}
+
+ >
)}
-
-
- {worker.filePath.split("\\").pop()?.split("/").pop() ?? worker.filePath}
-
-
- |
-
- {worker.ETA.startsWith("0:") ? worker.ETA.substring(2) : worker.ETA}
- |
-
-
- {worker.step}
-
- {Math.round(worker.percentage)}%
-
- |
-
- ))}
+ {Math.round(worker.percentage)}%
+
+
+
+ );
+ })}
diff --git a/packages/widgets/src/minecraft/server-status/component.tsx b/packages/widgets/src/minecraft/server-status/component.tsx
index 5ae024714..0d24784f7 100644
--- a/packages/widgets/src/minecraft/server-status/component.tsx
+++ b/packages/widgets/src/minecraft/server-status/component.tsx
@@ -36,7 +36,7 @@ export default function MinecraftServerStatusWidget({ options }: WidgetComponent
>
-
+
{title}
@@ -44,11 +44,13 @@ export default function MinecraftServerStatusWidget({ options }: WidgetComponent
{data.online && (
<>
-
+ {!options.isBedrockServer && (
+
+ )}
diff --git a/packages/widgets/src/modals/widget-advanced-options-modal.tsx b/packages/widgets/src/modals/widget-advanced-options-modal.tsx
index 674bc8f9c..f957b100a 100644
--- a/packages/widgets/src/modals/widget-advanced-options-modal.tsx
+++ b/packages/widgets/src/modals/widget-advanced-options-modal.tsx
@@ -1,6 +1,6 @@
"use client";
-import { Button, Group, Stack } from "@mantine/core";
+import { Button, CloseButton, ColorInput, Group, Stack, useMantineTheme } from "@mantine/core";
import { useForm } from "@homarr/form";
import { createModal } from "@homarr/modals";
@@ -15,6 +15,7 @@ interface InnerProps {
export const WidgetAdvancedOptionsModal = createModal(({ actions, innerProps }) => {
const t = useI18n();
+ const theme = useMantineTheme();
const form = useForm({
initialValues: innerProps.advancedOptions,
});
@@ -30,6 +31,18 @@ export const WidgetAdvancedOptionsModal = createModal(({ actions, in
label={t("item.edit.field.customCssClasses.label")}
{...form.getInputProps("customCssClasses")}
/>
+ color[6])}
+ rightSection={
+ form.setFieldValue("borderColor", "")}
+ style={{ display: form.getInputProps("borderColor").value ? undefined : "none" }}
+ />
+ }
+ {...form.getInputProps("borderColor")}
+ />