fix: restore dashboard widget transparency

This commit is contained in:
ajnart committed 2026-08-22 14:10:05 +02:00
1 parent 3f9ab07a5e
commit 54853b92a7
66 files changed
+511 -378

No files matched your search

@@ -19,35 +19,11 @@
align-self: start;
box-sizing: border-box;
height: var(--board-gutter-viewport-height);
padding-top: 24px;
margin-top: -24px;
overflow: clip;
}
.gutter[data-board-editing="true"] {
outline: 1px dashed var(--mantine-color-default-border);
outline-offset: -1px;
outline: 0.0625rem solid var(--mantine-color-default-border);
outline-offset: -0.0625rem;
border-radius: var(--mantine-radius-md);
}
.gutter[data-board-editing="true"]::before {
position: sticky;
top: 0;
inset-inline-start: 0;
z-index: 2;
display: block;
width: fit-content;
height: 24px;
padding-inline: var(--mantine-spacing-xs);
margin-top: -24px;
overflow: hidden;
font-size: var(--mantine-font-size-xs);
line-height: 24px;
color: var(--mantine-color-dimmed);
text-overflow: ellipsis;
white-space: nowrap;
pointer-events: none;
content: attr(aria-label);
background: color-mix(in srgb, var(--mantine-color-body) 88%, transparent);
border-radius: var(--mantine-radius-sm);
}
@@ -1,5 +1,6 @@
"use client";
import type { PropsWithChildren } from "react";
import { Box, Paper, Stack, Text } from "@mantine/core";
import { IconBulb } from "@tabler/icons-react";
@@ -22,6 +23,7 @@ import {
import { ScaledBoardCanvas } from "~/components/board/layout/scaled-board-canvas";
import { BoardEmptySection } from "~/components/board/sections/empty-section";
import { BoardGridEditorBoundary } from "~/components/board/sections/grid/board-grid-editor-boundary";
import { GridEditorRegistryProvider } from "~/components/board/sections/grid/grid-editor-registry";
import { BoardGridPortalHost } from "~/components/board/sections/grid/grid-portal-host";
import { BoardSectionCollapseProvider } from "~/components/board/sections/section-collapse";
import { BoardBackgroundVideo } from "~/components/layout/background";
@@ -31,6 +33,12 @@ import classes from "./_client.module.css";
const APP_SHELL_INLINE_PADDING = 32;
const BoardSelectionGridProvider = ({ children }: PropsWithChildren) => (
<GridEditorRegistryProvider>
<BoardSelectionProvider>{children}</BoardSelectionProvider>
</GridEditorRegistryProvider>
);
export const ClientBoard = () => {
const board = useRequiredBoard();
const t = useI18n("board.landmark");
@@ -68,7 +76,7 @@ export const ClientBoard = () => {
.join(" ");
const content = (
<BoardSelectionProvider>
<BoardSelectionGridProvider>
<BoardAdvancedFocusProvider>
<Box h="100%" pos="relative" data-homarr-dev-benchmark-board>
<BoardBackgroundVideo />
@@ -144,7 +152,7 @@ export const ClientBoard = () => {
</FloatingTip>
</Box>
</BoardAdvancedFocusProvider>
</BoardSelectionProvider>
</BoardSelectionGridProvider>
);
if (representativeWidth === null) return content;
@@ -157,30 +157,12 @@
border-bottom: 1px solid var(--mantine-color-default-border);
}
.panelIdentity {
min-width: 0;
flex: 1 1 auto;
}
.panelIdentityIcon {
flex: 0 0 auto;
}
.panelIdentityTitle,
.panelThreadTitle {
max-width: 14rem;
}
.panelActions {
min-width: 0;
flex: 0 0 auto;
margin-inline-start: auto;
}
.newConversationButton {
flex: 0 0 auto;
}
.panelAction {
width: 2.625rem;
height: 2.625rem;
@@ -207,21 +189,6 @@
display: none;
}
.newConversationButton {
width: 2.625rem;
max-width: 2.625rem;
height: 2.625rem;
padding-inline: 0.75rem;
}
.newConversationButton :global(.mantine-Button-section) {
margin-inline: 0;
}
.newConversationButton :global(.mantine-Button-label) {
display: none;
}
.messages {
padding: 0.5rem 0.65rem 1rem;
}
@@ -277,8 +244,7 @@
@container assistant-widget (max-width: 22rem) {
.panelAction,
.historyButton,
.newConversationButton {
.historyButton {
width: 2.25rem;
max-width: 2.25rem;
height: 2.25rem;
@@ -301,10 +267,6 @@
.composerContextLabel {
display: none;
}
.panelThreadTitle {
display: none;
}
}
@container assistant-widget (max-width: 18rem) {
@@ -2368,7 +2368,7 @@ const EmptyThread = () => {
<Box className={classes.empty}>
<Stack align="center" gap="lg" maw={560} w="100%">
<Stack align="center" gap="xs" maw={430}>
<ThemeIcon size={52} radius="xl" variant="light" color="red">
<ThemeIcon size={52} radius="xl" variant="light">
<IconRobot size={27} />
</ThemeIcon>
<Text size="xl" fw={700}>
@@ -2957,7 +2957,6 @@ const Composer = (props: ComposerProps) => {
) : (
<ComposerPrimitive.Send asChild>
<ActionIcon
color="red"
variant="filled"
size="lg"
aria-label={
@@ -3162,27 +3161,6 @@ interface AssistantConversationSurfaceProps extends AssistantConversationControl
onDismiss?: () => void;
}
const AssistantPanelIdentity = () => {
const t = useI18n("assistant");
const threadTitle = useAuiState((state) => state.threadListItem.title);
return (
<Group className={classes.panelIdentity} gap="xs" wrap="nowrap">
<ThemeIcon className={classes.panelIdentityIcon} variant="light" color="red" radius="xl" size="md">
<IconRobot size={16} />
</ThemeIcon>
<Box miw={0}>
<Text className={classes.panelIdentityTitle} size="sm" fw={700} truncate>
{t("title")}
</Text>
<Text className={classes.panelThreadTitle} size="xs" c="dimmed" truncate>
{threadTitle ?? t("newConversation")}
</Text>
</Box>
</Group>
);
};
export const AssistantConversationSurface = ({
isRunning,
pendingAction,
@@ -3212,23 +3190,20 @@ export const AssistantConversationSurface = ({
return (
<AssistantDirectiveEntitiesProvider>
<Group className={classes.panelHeader} justify="space-between" wrap="nowrap" gap="xs">
<AssistantPanelIdentity />
<Group className={classes.panelActions} gap={2} wrap="nowrap">
<ConversationHistory />
<ViewRefreshAction isRefreshing={isRefreshing} onRefresh={onRefresh} />
<AutoApprovalControl />
<Tooltip label={t("newConversation")}>
<ThreadListPrimitive.New asChild>
<Button
className={classes.newConversationButton}
<ActionIcon
className={classes.panelAction}
variant="subtle"
color="gray"
size="compact-sm"
leftSection={<IconPlus size={17} />}
aria-label={t("newConversation")}
>
{t("newConversation")}
</Button>
<IconPlus size={17} />
</ActionIcon>
</ThreadListPrimitive.New>
</Tooltip>
{onExpand && (
@@ -16,7 +16,7 @@ const AssistantPromptDetail = () => {
const t = useI18n("assistant");
return (
<Group px="md" py="sm" wrap="nowrap">
<ThemeIcon color="red" variant="light" radius="xl">
<ThemeIcon variant="light" radius="xl">
<IconRobot size={17} />
</ThemeIcon>
<Stack gap={1}>
@@ -137,7 +137,7 @@ const CompactAssistantWidget = ({
<Box className={classes.widgetPanel}>
<Group className={classes.compactState} justify="space-between" wrap="nowrap" gap="sm">
<Group wrap="nowrap" gap="xs" miw={0}>
<ThemeIcon variant="light" color="red" radius="xl" size="lg" flex="0 0 auto">
<ThemeIcon variant="light" radius="xl" size="lg" flex="0 0 auto">
<IconRobot size={19} />
</ThemeIcon>
<Stack className={classes.compactCopy} gap={1}>
@@ -167,13 +167,7 @@ interface AssistantWidgetStateProps {
children?: ReactNode;
}
const AssistantWidgetState = ({
icon: Icon,
title,
description,
color = "red",
children,
}: AssistantWidgetStateProps) => (
const AssistantWidgetState = ({ icon: Icon, title, description, color, children }: AssistantWidgetStateProps) => (
<Center className={classes.widgetState} h="100%" p="md">
<Stack align="center" gap="sm" ta="center" maw={360}>
<ThemeIcon variant="light" color={color} radius="xl" size="xl">
@@ -11,6 +11,7 @@ import { useI18n } from "@homarr/translation/client";
import type { Board, ContainerSectionItem, SectionItem } from "~/app/[locale]/boards/_types";
import { getLayoutRowCount } from "../layout";
import { resolvePinnedGridCollisions } from "../sections/grid/dnd";
import { useRegisteredGridEditors } from "../sections/grid/grid-editor-registry";
import { getSectionGridColumnCount, getSectionGridPlacements } from "../sections/grid/section-grid-placements";
import type { CommitSectionGridInput, SectionGridPlacement } from "../sections/grid/use-grid-layout-actions";
import { useGridLayoutActions } from "../sections/grid/use-grid-layout-actions";
@@ -57,26 +58,29 @@ export const ItemMoveModal = createModal<InnerProps>(({ actions, innerProps }) =
const tContainer = useI18n("section.container");
const tMoveResize = useI18n("item.moveResize");
const { board, commitSectionGrids, currentLayoutId, entry, sourceSectionId } = innerProps;
const registeredGridEditors = useRegisteredGridEditors();
const minimumSize = useMemo(
() => getEntryMinimumSize(board, currentLayoutId, entry),
[board, currentLayoutId, entry],
);
const targets = useMemo(
() =>
getMoveTargets(board, currentLayoutId, entry, {
canvas: tLandmark("canvas"),
container: tContainer("action.create"),
leftRail: tLandmark("leftRail"),
rightRail: tLandmark("rightRail"),
numbered: (name, index) => tMoveResize("target.numbered", { name, index: String(index) }),
located: (name, location, index) =>
tMoveResize("target.located", {
name,
location,
index: String(index),
}),
}),
[board, currentLayoutId, entry, tContainer, tLandmark, tMoveResize],
const targets = getMoveTargets(
board,
currentLayoutId,
entry,
{
canvas: tLandmark("canvas"),
container: tContainer("action.create"),
leftRail: tLandmark("leftRail"),
rightRail: tLandmark("rightRail"),
numbered: (name, index) => tMoveResize("target.numbered", { name, index: String(index) }),
located: (name, location, index) =>
tMoveResize("target.located", {
name,
location,
index: String(index),
}),
},
registeredGridEditors,
);
const targetById = useMemo(() => new Map(targets.map((target) => [target.id, target])), [targets]);
const initialTarget = targetById.get(sourceSectionId) ?? targets[0];
@@ -252,6 +256,7 @@ export const getMoveTargets = (
layoutId: string,
entry: MovableEntry,
labels: MoveTargetLabels,
registeredGridEditors?: ReadonlyMap<string, { placementMaxRowCount: number | null }>,
): MoveTarget[] => {
const minimum = getEntryMinimumSize(board, layoutId, entry);
@@ -260,13 +265,15 @@ export const getMoveTargets = (
const columnCount = getSectionColumnCount(board, layoutId, section.id);
const lane = getRootSectionLane(section.xOffset);
if (columnCount === 0) return [];
const registeredEditor = registeredGridEditors?.get(section.id);
if (lane !== "main" && registeredGridEditors && !registeredEditor) return [];
return minimum.width <= columnCount
? [
{
id: section.id,
name: lane === "left" ? labels.leftRail : lane === "right" ? labels.rightRail : labels.canvas,
columnCount,
maxRowCount: getSectionMaxRowCount(board, layoutId, section.id),
maxRowCount: registeredEditor?.placementMaxRowCount ?? getSectionMaxRowCount(board, layoutId, section.id),
},
]
: [];
@@ -3,8 +3,8 @@
The board uses fixed logical units:
- one `1 × 1` item is exactly `200 × 200` logical pixels;
- the gap is always `24` logical pixels, giving a `224` pixel track pitch;
- multi-cell items include the gaps between their cells (`2 × 1` is `424 × 200`);
- the gap matches Mantine's `sm` spacing at `12` logical pixels, giving a `212` pixel track pitch;
- multi-cell items include the gaps between their cells (`2 × 1` is `412 × 200`);
- the canvas is zoomed once with one uniform scale derived from its available width.
Items keep the same logical dimensions at every viewport. The viewport reserves
@@ -3,7 +3,7 @@
* individual cards never change their internal layout at different viewports.
*/
export const LOGICAL_GRID_CELL_SIZE = 200;
export const LOGICAL_GRID_GAP = 24;
export const LOGICAL_GRID_GAP = 12;
export const LOGICAL_GRID_PITCH = LOGICAL_GRID_CELL_SIZE + LOGICAL_GRID_GAP;
export const COLLAPSED_SECTION_ROW_COUNT = 0.5;
@@ -23,8 +23,8 @@
* zoom and enlarges everything together.
*/
--mantine-scale: var(--board-canvas-ui-scale, 1);
/* Keep the legacy card inset visually stable while the fixed canvas is zoomed. */
--board-grid-item-inset: calc(10px * var(--board-canvas-inverse-scale, 1) - 12px);
/* Keep a subtle boundary inset without widening the visual gap too much. */
--board-grid-item-inset: calc(var(--mantine-spacing-xs) / 2);
--mantine-spacing-xs: calc(0.625rem * var(--board-canvas-ui-scale, 1));
--mantine-spacing-sm: calc(0.75rem * var(--board-canvas-ui-scale, 1));
--mantine-spacing-md: calc(1rem * var(--board-canvas-ui-scale, 1));
@@ -9,18 +9,18 @@ import {
} from "../index";
describe("fixed dashboard geometry", () => {
test("keeps a 1x1 item exactly 200px with a fixed 24px gap", () => {
test("keeps a 1x1 item exactly 200px with a fixed 12px gap", () => {
expect(getLogicalTrackSize(1)).toBe(200);
expect(getLogicalTrackSize(0.5)).toBe(88);
expect(getLogicalTrackSize(2)).toBe(424);
expect(getLogicalTrackSize(3)).toBe(648);
expect(getLogicalTrackSize(0.5)).toBe(94);
expect(getLogicalTrackSize(2)).toBe(412);
expect(getLogicalTrackSize(3)).toBe(624);
expect(getLogicalItemStyle({ x: 1, y: 2, w: 2, h: 3 })).toEqual({
position: "absolute",
left: 224,
top: 448,
width: 424,
height: 648,
left: 212,
top: 424,
width: 412,
height: 624,
});
});
@@ -35,7 +35,7 @@ describe("fixed dashboard geometry", () => {
expect(getGridRowCountForVisualHeight(1200, 1)).toBe(6);
expect(getLogicalTrackSize(getGridRowCountForVisualHeight(1200, 1))).toBeGreaterThanOrEqual(1200);
expect(getGridRowCountForVisualHeight(1200, 0.5)).toBe(11);
expect(getGridRowCountForVisualHeight(1200, 0.5)).toBe(12);
expect(getLogicalTrackSize(getGridRowCountForVisualHeight(1200, 0.5)) * 0.5).toBeGreaterThanOrEqual(1200);
});
});
@@ -42,7 +42,18 @@ export const BoardContainerSection = ({ section }: Props) => {
const labelRight = isEditMode ? 48 : options.showOpenAll ? 40 : 8;
return (
<Box className="board-grid-item-content" data-grid-item-content w="100%" h="100%" style={{ overflow: "visible" }}>
<Box
className={combineClasses("board-grid-item-content", classes.containerRoot)}
pos="relative"
data-grid-item-content
w="100%"
h="100%"
style={{
overflow: "visible",
"--opacity": board.opacity / 100,
"--border-color": options.borderColor || undefined,
}}
>
<Card
className={combineClasses(
classes.itemCard,
@@ -55,8 +66,6 @@ export const BoardContainerSection = ({ section }: Props) => {
styles={{
root: {
overflow: "visible",
"--opacity": board.opacity / 100,
"--border-color": options.borderColor || undefined,
},
}}
radius={board.itemRadius}
@@ -66,7 +75,7 @@ export const BoardContainerSection = ({ section }: Props) => {
<Button
className={classes.containerToggle}
pos="absolute"
top={isVisuallyCollapsed ? 0 : -24}
top={isVisuallyCollapsed ? 0 : "calc(var(--mantine-spacing-xs) * -1)"}
left={0}
w="100%"
h={isVisuallyCollapsed ? "100%" : 24}
@@ -101,35 +110,10 @@ export const BoardContainerSection = ({ section }: Props) => {
)}
</Button>
)}
{!isVisuallyCollapsed && !options.collapsible && options.showLabel && options.title && (
<Badge
pos="absolute"
top={-24}
left={labelLeft}
maw={`calc(100% - ${labelLeft + labelRight}px)`}
size="md"
radius="sm"
variant="default"
c="var(--mantine-color-text)"
style={{
zIndex: 9,
overflow: "hidden",
pointerEvents: "none",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
backgroundColor: "var(--background-color)",
borderColor: "var(--border-color)",
}}
title={options.title}
data-board-container-label
>
{options.title}
</Badge>
)}
{options.showOpenAll && !isEditMode && (
<ActionIcon
pos="absolute"
top={isVisuallyCollapsed ? "50%" : -24}
top={isVisuallyCollapsed ? "50%" : "calc(var(--mantine-spacing-xs) * -1)"}
right={8}
style={{ zIndex: 10, transform: isVisuallyCollapsed ? "translateY(-50%)" : undefined }}
variant={options.collapsible ? "subtle" : "default"}
@@ -154,6 +138,24 @@ export const BoardContainerSection = ({ section }: Props) => {
<SectionGrid section={section} columnCount={section.width} requestedRowCount={section.height} label={label} />
</Box>
</Card>
{!isVisuallyCollapsed && !options.collapsible && options.showLabel && options.title && (
<Badge
className={classes.containerLabel}
pos="absolute"
top="calc(var(--mantine-spacing-lg) * -1 - 2px)"
left={labelLeft}
maw={`calc(100% - ${labelLeft + labelRight}px)`}
size="md"
radius="sm"
fz="md"
variant="default"
c="var(--mantine-color-text)"
title={options.title}
data-board-container-label
>
{options.title}
</Badge>
)}
{isEditMode && <BoardContainerMenu section={section} />}
</Box>
);
@@ -7,7 +7,7 @@ import { createPortal } from "react-dom";
import { useEditMode } from "@homarr/boards/edit-mode";
import { getLoadedGridEditorModule, loadGridEditorAsync } from "./grid-editor-loader";
import { GridEditorRegistryProvider, useRegisteredGridEditors } from "./grid-editor-registry";
import { useRegisteredGridEditors } from "./grid-editor-registry";
import { GridEditorRuntimeProvider } from "./grid-editor-runtime";
type BoardGridEditorProviderComponent = ComponentType<PropsWithChildren>;
@@ -54,14 +54,14 @@ export const BoardGridEditorBoundary = ({ children }: PropsWithChildren) => {
const GridEditor = resolvedEditorModule?.default ?? null;
return (
<GridEditorRegistryProvider>
<>
<GridEditorRuntimeProvider status={runtimeStatus}>{children}</GridEditorRuntimeProvider>
{isEditMode && Provider && GridEditor && (
<Provider>
<RegisteredGridEditors GridEditor={GridEditor} />
</Provider>
)}
</GridEditorRegistryProvider>
</>
);
};
@@ -23,7 +23,7 @@ describe("dnd grid coordinates", () => {
test("uses the dragged shape origin so a non-center grab retains its pointer offset", () => {
const scale = 0.75;
const target = { left: -280, top: 390, width: getLogicalTrackSize(5) * scale };
const initialShape = { left: target.left, top: target.top, width: 424 * scale, height: 200 * scale };
const initialShape = { left: target.left, top: target.top, width: 412 * scale, height: 200 * scale };
const pointerDelta = { x: LOGICAL_GRID_PITCH * scale * 2, y: LOGICAL_GRID_PITCH * scale * 3 };
expect(
@@ -55,6 +55,7 @@ export interface GridEditorRenderProps {
}
export interface RegisteredGridEditor extends GridEditorRenderProps {
placementMaxRowCount: number | null;
host: HTMLElement;
disabled: boolean;
}
@@ -155,17 +155,6 @@ body[data-board-grid-interacting="resize"] .board-grid-entry[data-dnd-active="tr
box-shadow: inset 0 0 0 2px var(--mantine-color-red-filled);
}
[data-section-kind="empty"][data-rail-placement="main"] > .board-grid-editor[data-dnd-drop-target="true"] {
outline: 2px solid color-mix(in srgb, var(--mantine-primary-color-filled) 60%, transparent);
outline-offset: 4px;
box-shadow: none;
}
[data-section-kind="empty"][data-rail-placement="main"]
> .board-grid-editor[data-dnd-drop-target="true"][data-dnd-drop-valid="false"] {
outline-color: var(--mantine-color-red-filled);
}
[data-board-container-collapsed]
> [data-board-container-body]
> [data-section-id]
@@ -50,6 +50,10 @@
overflow: visible;
}
.staticItem:has([data-board-container-label]) {
z-index: 2;
}
.editorEntry {
overflow: visible;
cursor: grab;
@@ -130,6 +130,10 @@ export const SectionGrid = ({
const contentRowCount = Math.max(1, getLayoutRowCount(displayPlacements));
const rowCount = Math.max(contentRowCount, requestedRowCount, minimumViewportRowCount);
const maxRowCount = section.kind === "container" || railPlacement !== "main" ? rowCount : null;
let placementMaxRowCount = maxRowCount;
if (railPlacement !== "main") {
placementMaxRowCount = minimumViewportRowCount;
}
// A scrollable container isn't forced to grow with its content - it scrolls internally instead
// of expanding to fit every widget, so its viewport height is capped independently of rowCount.
const isScrollableContainer = section.kind === "container" && section.options.scrollable;
@@ -175,6 +179,7 @@ export const SectionGrid = ({
columnCount,
rowCount,
maxRowCount,
placementMaxRowCount,
placements: displayPlacements,
transactionPlacements: placements,
className: editorClassName,
@@ -190,6 +195,7 @@ export const SectionGrid = ({
entryElementStore,
isInteractionDisabled,
maxRowCount,
placementMaxRowCount,
placements,
rowCount,
section,
@@ -1,4 +1,4 @@
.itemCard {
.containerRoot {
@mixin dark {
--background-color: rgb(from var(--mantine-color-dark-6) r g b / var(--opacity));
--border-color: rgb(from var(--mantine-color-dark-4) r g b / var(--opacity));
@@ -7,6 +7,9 @@
--background-color: rgb(from var(--mantine-color-white) r g b / var(--opacity));
--border-color: rgb(from var(--mantine-color-gray-3) r g b / var(--opacity));
}
}
.itemCard {
background-color: var(--background-color) !important;
border-color: var(--border-color) !important;
}
@@ -42,6 +45,21 @@
white-space: nowrap;
}
.containerLabel {
overflow: visible;
pointer-events: none;
background-color: var(--background-color);
border-color: var(--border-color);
}
.containerLabel :global(.mantine-Badge-label) {
min-width: 0;
overflow: hidden;
line-height: 1.2;
text-overflow: ellipsis;
white-space: nowrap;
}
.containerBody {
visibility: visible;
opacity: 1;
@@ -31,7 +31,7 @@ interface BoardSelectionContextValue {
copySelectedItems: () => Promise<void>;
pasteItems: () => Promise<void>;
removeSelectedItems: () => void;
moveSelectedItemsToSection: (sectionId: string) => void;
moveSelectedItemsToSection: (sectionId: string, maxRowCount?: number | null) => void;
}
const BoardSelectionContext = createContext<BoardSelectionContextValue | null>(null);
@@ -229,7 +229,7 @@ export const BoardSelectionProvider = ({ children }: PropsWithChildren) => {
}, [board.items, clearSelection, persistBoard, selectedItemIds, tCommon, tSelection]);
const moveSelectedItemsToSection = useCallback(
(targetSectionId: string) => {
(targetSectionId: string, maxRowCount?: number | null) => {
if (selectedItemIds.size === 0) return;
const idsToMove = new Set(selectedItemIds);
@@ -248,7 +248,7 @@ export const BoardSelectionProvider = ({ children }: PropsWithChildren) => {
targetSection.kind === "container"
? (containerLayout?.width ?? 0)
: getBoardLaneColumnCount(boardLayout, getRootSectionLane(targetSection.xOffset));
const rowCount = containerLayout?.height ?? 9999;
const rowCount = containerLayout?.height ?? maxRowCount ?? 9999;
if (columnCount === 0 || rowCount === 0) return previous;
const existingContainers = previous.sections
@@ -25,6 +25,7 @@ import { useI18n } from "@homarr/translation/client";
import { widgetCatalogIcons } from "@homarr/widgets/catalog";
import type { EmptySection } from "~/app/[locale]/boards/_types";
import { useRegisteredGridEditors } from "~/components/board/sections/grid/grid-editor-registry";
import { useBoardSelection } from "./board-selection-context";
const MAX_AVATARS_DISPLAYED = 5;
@@ -40,6 +41,7 @@ export const BoardSelectionToolbar = () => {
moveSelectedItemsToSection,
} = useBoardSelection();
const board = useRequiredBoard();
const registeredGridEditors = useRegisteredGridEditors();
const currentLayoutId = useCurrentLayout();
const currentLayout = board.layouts.find((layout) => layout.id === currentLayoutId);
const t = useI18n();
@@ -96,7 +98,15 @@ export const BoardSelectionToolbar = () => {
.at(0);
const placementOptions = [
...(mainCanvasSection ? [{ value: mainCanvasSection.id, label: t("item.create.destination.mainCanvas") }] : []),
...(mainCanvasSection
? [
{
value: mainCanvasSection.id,
label: t("item.create.destination.mainCanvas"),
maxRowCount: null,
},
]
: []),
...board.sections.flatMap((section) => {
if (section.kind !== "empty") return [];
const lane = getRootSectionLane(section.xOffset);
@@ -105,6 +115,7 @@ export const BoardSelectionToolbar = () => {
{
value: section.id,
label: t(`item.create.destination.${lane === "left" ? "leftRail" : "rightRail"}`),
maxRowCount: registeredGridEditors.get(section.id)?.placementMaxRowCount,
},
];
}),
@@ -113,6 +124,7 @@ export const BoardSelectionToolbar = () => {
.map((section) => ({
value: section.id,
label: section.options.title.trim() || t("section.container.untitled"),
maxRowCount: section.layouts.find((layout) => layout.layoutId === currentLayoutId)?.height,
})),
];
@@ -244,7 +256,11 @@ export const BoardSelectionToolbar = () => {
<Menu.Dropdown>
<Menu.Label>{tSelection("moveTo")}</Menu.Label>
{placementOptions.map((opt) => (
<Menu.Item key={opt.value} onClick={() => moveSelectedItemsToSection(opt.value)}>
<Menu.Item
key={opt.value}
disabled={opt.maxRowCount === undefined}
onClick={() => moveSelectedItemsToSection(opt.value, opt.maxRowCount)}
>
{opt.label}
</Menu.Item>
))}
@@ -82,7 +82,7 @@ const ArchiveTeamWarriorWidgetContent = ({
</Group>
{(isAdvanced || options.showBroadcastMessage) && status.broadcastMessage && layout.showBroadcast && (
<Card withBorder p="xs">
<Card withBorder p="xs" bg="transparent" style={{ borderColor: neutralSurfaceBorder }}>
<Text size="xs" lineClamp={isAdvanced ? undefined : 3}>
{status.broadcastMessage}
</Text>
@@ -143,7 +143,13 @@ const ArchiveTeamWarriorWidgetContent = ({
{status.items.map((item) => {
const itemStatusKey = getStatusKey(item.status);
return (
<Card key={item.id} withBorder p="xs">
<Card
key={item.id}
withBorder
p="xs"
bg="transparent"
style={{ borderColor: neutralSurfaceBorder }}
>
<Group justify="space-between" wrap="nowrap">
<Stack gap={0} miw={0}>
<Text size="sm" fw={600} lineClamp={1}>
@@ -201,6 +207,9 @@ export const getArchiveCompactLayout = (width: number, height: number, isAdvance
const formatBandwidth = (value?: number) => formatByteRate(Math.round(value ?? 0));
const neutralSurfaceBorder =
"rgb(from var(--mantine-color-default-border) r g b / calc(var(--opacity, 1) * 0.45))";
type WarriorStatusKey = "running" | "completed" | "failed" | "canceled" | "stopped" | "idle" | "unknown";
const getStatusKey = (status?: string | null): WarriorStatusKey => {
+1 -1
View File
@@ -17,7 +17,7 @@ export default function AssistantWidget(props: WidgetComponentProps<"assistant">
return (
<Center h="100%" p="md">
<Stack align="center" gap="xs" ta="center">
<ThemeIcon variant="light" color="red" radius="xl" size="lg">
<ThemeIcon variant="light" radius="xl" size="lg">
<IconRobot size="var(--mantine-font-size-xl)" />
</ThemeIcon>
<Text size="sm" fw={600}>
@@ -40,7 +40,7 @@
gap: clamp(2px, 1.5cqi, 4px);
padding: clamp(4px, 2cqi, 8px);
border-radius: var(--mantine-radius-md);
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
background-color: transparent;
overflow: hidden;
}
@@ -29,7 +29,7 @@
gap: clamp(2px, 1.5cqi, 5px);
padding: clamp(4px, 2.5cqi, 10px);
border-radius: var(--mantine-radius-md);
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
background-color: transparent;
overflow: hidden;
}
@@ -1,7 +1,3 @@
.progress {
opacity: max(0.6, var(--opacity, 1));
}
.clickableCard {
position: relative;
transition: background-color 150ms;
@@ -159,7 +159,6 @@ const MetricValue = ({ value, progress, size }: Pick<MetricRowProps, "value" | "
</Text>
{progress && (
<Progress
className={classes.progress}
value={progress.value}
color={progress.color}
size={getProgressTrackSize(size.progressSize)}
@@ -1,7 +1,3 @@
.progress {
opacity: max(0.6, var(--opacity, 1));
}
.target {
position: relative;
z-index: 2;
@@ -64,7 +64,6 @@ export const DiskUsage = ({ system, fontSize, progressSize, valueMiw, valueGap =
>
<Box pos="relative" style={{ flex: 1, minWidth: 24 }}>
<Progress
className={classes.progress}
value={system.disk}
color={thresholdColor(system.disk)}
size={trackSize}
@@ -107,7 +106,6 @@ export const DiskUsage = ({ system, fontSize, progressSize, valueMiw, valueGap =
{formatPercent(value)}
</Text>
<Progress
className={classes.progress}
value={value}
color={thresholdColor(value)}
size="xs"
@@ -123,7 +121,6 @@ export const DiskUsage = ({ system, fontSize, progressSize, valueMiw, valueGap =
<Box style={{ display: "flex", alignItems: "center", flex: 1, minWidth: 24 }}>
<Box pos="relative" style={{ flex: 1, minWidth: 24 }}>
<Progress
className={classes.progress}
value={system.disk}
color={thresholdColor(system.disk)}
size={trackSize}
@@ -1,5 +1,15 @@
.card:hover {
background-color: var(--mantine-color-primaryColor-light-hover);
.card {
background-color: transparent;
border-color: rgb(from var(--mantine-color-secondaryColor-filled) r g b / calc(var(--opacity, 1) * 0.38));
transition:
background-color 120ms ease,
border-color 120ms ease;
}
.card:hover,
.card:focus-within {
background-color: transparent;
border-color: rgb(from var(--mantine-color-secondaryColor-filled) r g b / calc(var(--opacity, 1) * 0.62));
}
.bookmarkButton:focus-visible {
@@ -8,14 +18,6 @@
border-radius: var(--mantine-radius-sm);
}
[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 {
background-color: var(--mantine-color-iconColor-filled-hover);
}
@@ -23,3 +25,9 @@
.bookmarkButton:focus-visible .bookmarkIcon {
background-color: var(--mantine-color-iconColor-filled-hover);
}
@media (prefers-reduced-motion: reduce) {
.card {
transition: none;
}
}
+11 -3
View File
@@ -16,7 +16,6 @@ import {
UnstyledButton,
} from "@mantine/core";
import { IconLink } from "@tabler/icons-react";
import combineClasses from "clsx";
import { clientApi } from "@homarr/api/client";
import { useRequiredBoard } from "@homarr/boards/context";
@@ -216,6 +215,7 @@ const FlexLayout = ({
<Card
radius={board.itemRadius}
className={classes.card}
bg="transparent"
w="100%"
display="flex"
p={4}
@@ -300,7 +300,14 @@ const AdvancedBookmarksLayout = ({
target={href ? (openNewTab ? "_blank" : "_self") : undefined}
rel={href && openNewTab ? SAFE_NEW_TAB_REL : undefined}
>
<Card radius={board.itemRadius} className={classes.card} withBorder={withBorder} p="md" h="100%">
<Card
radius={board.itemRadius}
className={classes.card}
bg="transparent"
withBorder={withBorder}
p="md"
h="100%"
>
<Group align="flex-start" wrap="nowrap">
<BookmarkIcon
app={app}
@@ -381,7 +388,8 @@ const GridLayout = ({
>
<Card
h="100%"
className={combineClasses(classes.card, classes["card-grid"])}
className={classes.card}
bg="transparent"
radius={board.itemRadius}
withBorder={withBorder}
p="xs"
@@ -135,14 +135,13 @@ export function HomarrDataTable<T>({
withTableBorder={false}
borderRadius={0}
highlightOnHover
striped="odd"
stripedColor={{ dark: "dark.7", light: "gray.0" }}
highlightOnHoverColor={{ dark: "dark.5", light: "gray.1" }}
verticalAlign="center"
loaderBackgroundBlur={2}
textSelectionDisabled
height="100%"
{...props}
striped={false}
columns={fittedColumns}
onMouseDownCapture={handleMouseDownCapture}
className={mergedClassName}
@@ -0,0 +1,29 @@
.accordion {
--coolify-neutral-fill: rgb(
from var(--mantine-color-default-hover) r g b / calc(var(--opacity, 1) * 0.12)
);
--coolify-neutral-border: rgb(
from var(--mantine-color-default-border) r g b / calc(var(--opacity, 1) * 0.45)
);
}
.accordion :global(.mantine-Accordion-item) {
--item-border-color: var(--coolify-neutral-border);
--item-filled-color: var(--coolify-neutral-fill);
}
.accordion :global(.mantine-Accordion-control) {
background-color: transparent;
}
.accordion :global(.mantine-Accordion-control:not(:disabled, [data-disabled]):hover) {
background-color: var(--coolify-neutral-fill);
}
.neutralDividerBottom {
border-bottom: 1px solid var(--coolify-neutral-border);
}
.neutralDividerTop {
border-top: 1px solid var(--coolify-neutral-border);
}
@@ -8,6 +8,7 @@ import { useTimeAgo } from "@homarr/common";
import { useI18n } from "@homarr/translation/client";
import { ApplicationsSection } from "./applications-section";
import classes from "./component.module.css";
import { getSafeApplicationUrl, SAFE_NEW_TAB_REL } from "../common/application-url";
import { buildServerResourceCounts, getBadgeColor, isCoolifyServerOnline, parseStatus } from "./coolify-utils";
import { ServersSection } from "./servers-section";
@@ -71,12 +72,12 @@ export function InstanceCard({ instance, options, isTiny, isAdvanced, widgetKey,
.join(" · ");
return (
<Card p={0} radius="sm">
<Card p={0} radius="sm" bg="transparent">
<Group
p="xs"
justify="space-between"
wrap="nowrap"
style={{ borderBottom: "1px solid var(--mantine-color-dark-4)" }}
className={classes.neutralDividerBottom}
>
<Group gap={4} wrap="nowrap" miw={0}>
<Image src={COOLIFY_ICON_URL} alt="Coolify" w={16} h={16} />
@@ -135,6 +136,7 @@ export function InstanceCard({ instance, options, isTiny, isAdvanced, widgetKey,
</Group>
<Accordion
className={classes.accordion}
variant="filled"
chevronPosition="right"
multiple
@@ -175,11 +177,7 @@ const InstanceFooter = ({ version, updatedAt }: { version: string; updatedAt: Da
const t = useI18n("widget.coolify");
const relativeTime = useTimeAgo(updatedAt, 60_000);
return (
<Group
justify="space-between"
p={4}
style={{ borderTop: "1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4))" }}
>
<Group justify="space-between" p={4} className={classes.neutralDividerTop}>
<Text size="10px" c="dimmed">
v{version}
</Text>
@@ -8,6 +8,7 @@ import { useTimeAgo } from "@homarr/common";
import { useI18n } from "@homarr/translation/client";
import { ApplicationsSection } from "./applications-section";
import classes from "./component.module.css";
import { getSafeApplicationUrl, SAFE_NEW_TAB_REL } from "../common/application-url";
import { buildServerResourceCounts } from "./coolify-utils";
import { ServersSection } from "./servers-section";
@@ -82,6 +83,7 @@ export function SingleInstanceLayout({
</Group>
<Accordion
className={classes.accordion}
variant="contained"
chevronPosition="right"
multiple
@@ -123,11 +125,7 @@ const InstanceFooter = ({ version, updatedAt }: { version: string; updatedAt: Da
const t = useI18n("widget.coolify");
const relativeTime = useTimeAgo(updatedAt, 60_000);
return (
<Group
justify="space-between"
p={4}
style={{ borderTop: "1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4))" }}
>
<Group justify="space-between" p={4} className={classes.neutralDividerTop}>
<Group gap={2}>
<Image src={COOLIFY_ICON_URL} alt="Coolify" w={16} h={16} />
<Text size="xs" c="dimmed">
@@ -1,7 +1,3 @@
[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);
.card {
background-color: transparent;
}
+3 -1
View File
@@ -27,6 +27,7 @@ import type { WidgetComponentProps } from "../definition";
import { calculateBandwidth, formatBitsPerSec } from "./bandwidth";
import { FirewallMenu } from "./firewall-menu";
import { FirewallVersion, formatVersion } from "./firewall-version";
import classes from "./firewall.module.css";
export interface Firewall {
label: string;
@@ -243,7 +244,7 @@ const FirewallPanel = ({
t,
tCommon,
}: FirewallPanelProps) => (
<Paper withBorder={isAdvanced} radius="sm" p={isAdvanced ? "xs" : 0}>
<Paper className={classes.card} withBorder={isAdvanced} radius="sm" p={isAdvanced ? "xs" : 0} bg="transparent">
<Stack gap="xs">
{isAdvanced && (
<Group justify="space-between" wrap="nowrap">
@@ -382,6 +383,7 @@ const InterfacesPanel = ({
value={isAdvanced ? "interfaces" : accordionValue}
onChange={isAdvanced ? undefined : setAccordionValue}
variant={isAdvanced ? "contained" : "default"}
classNames={{ item: classes.accordionItem, control: classes.accordionControl }}
>
<Accordion.Item value="interfaces">
<Accordion.Control icon={<IconTopologyBus size="var(--mantine-font-size-md)" />}>
@@ -1,6 +1,7 @@
import { Box, Select } from "@mantine/core";
import type { Firewall } from "./component";
import classes from "./firewall.module.css";
interface FirewallMenuProps {
onChange: (value: string | null) => void;
@@ -18,6 +19,7 @@ export const FirewallMenu = ({ onChange, isTiny, dropdownItems, selectedFirewall
w="100%"
color="lightgray"
data={dropdownItems}
classNames={{ input: classes.selectInput }}
styles={{
input: {
minHeight: "24px",
@@ -1,7 +1,33 @@
[data-mantine-color-scheme="light"] .card {
background-color: var(--mantine-color-gray-1);
.card {
background-color: transparent;
border-color: rgb(from var(--mantine-color-default-border) r g b / calc(var(--opacity, 1) * 0.45));
}
[data-mantine-color-scheme="dark"] .card {
background-color: var(--mantine-color-dark-7);
.accordionItem {
border-color: rgb(from var(--mantine-color-default-border) r g b / calc(var(--opacity, 1) * 0.45));
background-color: transparent;
}
.accordionItem[data-active] {
background-color: transparent;
}
.accordionControl {
background-color: transparent;
}
.accordionControl:hover,
.accordionControl:focus-visible,
.accordionControl:active {
background-color: rgb(from var(--mantine-color-default-hover) r g b / calc(var(--opacity, 1) * 0.4));
}
.selectInput {
background-color: transparent;
border-color: rgb(from var(--mantine-color-default-border) r g b / calc(var(--opacity, 1) * 0.45));
}
.selectInput:focus,
.selectInput:focus-within {
border-color: var(--mantine-primary-color-filled);
}
@@ -1,9 +1,5 @@
[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);
.card {
background-color: transparent;
}
.systemPanel {
@@ -191,7 +191,7 @@ export const SystemHealthMonitoring = ({
))}
</Flex>
{isAdvanced && (
<Card radius={board.itemRadius} p="sm">
<Card className={classes.card} radius={board.itemRadius} p="sm">
<SystemInformationList healthInfo={healthInfo} memoryUsage={memoryUsage} t={t} compact />
</Card>
)}
@@ -4,23 +4,19 @@
display: flex;
text-decoration: none;
color: inherit;
border: 1px solid var(--border-color);
background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7));
border: 1px solid
rgb(from var(--mantine-color-default-border) r g b / calc(var(--opacity, 1) * 0.45));
background-color: transparent;
transition:
border-color 120ms ease,
background-color 120ms ease;
@mixin dark {
--border-color: var(--mantine-color-dark-4);
}
@mixin light {
--border-color: var(--mantine-color-gray-3);
}
&:hover,
&:focus-visible {
border-color: var(--mantine-primary-color-filled);
background-color: var(--mantine-color-default-hover);
background-color: rgb(
from var(--mantine-color-default-hover) r g b / calc(var(--opacity, 1) * 0.14)
);
}
&:focus-visible {
@@ -29,6 +25,16 @@
}
}
:global(.mediaMissing-wrapper) :global(.mantine-SimpleGrid-root) > :global(.mantine-Paper-root) {
border-color: rgb(from var(--mantine-color-default-border) r g b / calc(var(--opacity, 1) * 0.45));
background-color: transparent;
}
:global(.mediaMissing-wrapper) :global(.mantine-Tabs-list) {
--tab-border-color: rgb(from var(--mantine-color-default-border) r g b / calc(var(--opacity, 1) * 0.45));
--tab-hover-color: rgb(from var(--mantine-color-default-hover) r g b / calc(var(--opacity, 1) * 0.14));
}
.content {
position: relative;
z-index: 1;
@@ -36,11 +36,13 @@ import classes from "./component.module.css";
import type { MediaMissingTab } from "./tabs";
import { resolveMediaMissingTab } from "./tabs";
const MIN_CARD_WIDTH = 280;
const MAX_COLUMNS = 4;
export default function MediaMissingWidget({
integrationIds,
options,
width,
height,
displayMode,
widgetRuntimeRef,
}: WidgetComponentProps<"mediaMissing">) {
@@ -88,9 +90,7 @@ export default function MediaMissingWidget({
const enabledPanelCount = Number(showMissing) + Number(showQueued);
const panelWidth = isAdvanced && enabledPanelCount > 1 ? width / enabledPanelCount : width;
const isThin = !isAdvanced && panelWidth > 0 && panelWidth < 160;
const isShort = !isAdvanced && height > 0 && height < 180;
const targetCardWidth = isShort ? 130 : 200;
const columns = panelWidth > 0 ? Math.max(1, Math.min(Math.floor(panelWidth / targetCardWidth), 4)) : 1;
const columns = panelWidth > 0 ? Math.max(1, Math.min(Math.floor(panelWidth / MIN_CARD_WIDTH), MAX_COLUMNS)) : 1;
const density: Density = isThin ? "thin" : panelWidth > 0 && panelWidth / columns < 180 ? "compact" : "comfortable";
const tabLabel = (label: string, shown: number, total: number) => (isThin ? total : `${label} (${shown}/${total})`);
@@ -114,7 +114,7 @@ export default function MediaMissingWidget({
{emptyLabel}
</Text>
) : (
<SimpleGrid cols={columns} spacing="xs" verticalSpacing="xs">
<SimpleGrid cols={Math.min(columns, entries.length)} spacing="xs" verticalSpacing="xs">
{entries.map(({ item, integrationId }) => (
<MediaCard
key={`${integrationId}-${item.type}-${item.id}`}
@@ -1,9 +1,5 @@
[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);
.card {
background-color: transparent;
}
.userCard {
@@ -13,7 +9,7 @@
&:hover,
&:focus-visible {
background-color: var(--mantine-color-default-hover);
background-color: transparent;
}
&:focus-visible {
@@ -5,17 +5,16 @@ import type { ReactNode } from "react";
import { getQueryKey } from "@trpc/react-query";
import {
Box,
Center,
Button,
Divider,
Group,
Pagination,
Paper,
ScrollArea,
SegmentedControl,
Select,
SimpleGrid,
Stack,
Text,
VisuallyHidden,
} from "@mantine/core";
import { IconClipboardList, IconCpu2, IconReportAnalytics } from "@tabler/icons-react";
@@ -147,35 +146,45 @@ export default function MediaTranscodingWidget({
)}
<Divider />
<Group gap="xs" mb={4} ms={4} me={8} wrap="nowrap">
<SegmentedControl
data={views.map((value) => {
const Icon = viewIcons[value];
return {
label: (
<Center style={{ gap: 4 }}>
<Icon size="var(--mantine-font-size-xs)" style={{ flexShrink: 0 }} />
{footerLayout.showTabLabels ? (
<Text span size="xs">
{t(`tab.${value}`)}
</Text>
) : (
<VisuallyHidden>{t(`tab.${value}`)}</VisuallyHidden>
)}
</Center>
),
{width >= 560 ? (
<Button.Group style={{ flex: 1, minWidth: 0 }}>
{views.map((value) => {
const Icon = viewIcons[value];
return (
<Button
key={value}
size="compact-xs"
variant={value === view ? "filled" : "default"}
leftSection={<Icon size="var(--mantine-font-size-xs)" />}
aria-pressed={value === view}
style={{ flex: 1 }}
onClick={() => setView(value)}
>
{t(`tab.${value}`)}
</Button>
);
})}
</Button.Group>
) : (
<Select
size="xs"
value={view}
data={views.map((value) => ({
value,
};
})}
value={view}
onChange={(value) => {
const nextView = viewBySegmentValue[value];
if (nextView) {
setView(nextView);
}
}}
size="xs"
style={{ minWidth: 0, flexShrink: footerLayout.showTabLabels ? 1 : 0 }}
/>
label: t(`tab.${value}`),
}))}
aria-label={t("option.defaultView.label")}
style={{ flex: 1, minWidth: 0 }}
onChange={(value) => {
if (!value) return;
const nextView = viewBySegmentValue[value];
if (nextView) {
setView(nextView);
}
}}
/>
)}
<Group gap="xs" ml="auto" wrap="nowrap">
{view === "queue" && queuePageCount > 1 && (
@@ -97,7 +97,7 @@ function StatisticItem(props: StatisticItemProps) {
const board = useRequiredBoard();
return (
<Tooltip label={props.label}>
<Card p={0} radius={board.itemRadius} miw={48} flex={1}>
<Card p={0} radius={board.itemRadius} miw={48} flex={1} bg="transparent">
<Group justify="center" align="center" gap="xs" w="100%" wrap="nowrap">
<props.icon size="var(--mantine-font-size-md)" style={{ flexShrink: 0 }} />
<Text size="md">{props.value}</Text>
@@ -14,6 +14,9 @@ import { getNetworkControllerStatusLayout } from "./layout";
import { WifiVariant } from "./variants/wifi-variant";
import { WiredVariant } from "./variants/wired-variant";
const neutralBorderColor =
"rgb(from var(--mantine-color-default-border) r g b / calc(var(--opacity, 1) * 0.45))";
export default function NetworkControllerNetworkStatusWidget({
options,
integrationIds,
@@ -69,7 +72,7 @@ export default function NetworkControllerNetworkStatusWidget({
{queryIndicators}
<SimpleGrid cols={summaries.length > 1 ? layout.sourceColumns : 1} spacing="sm">
{summaries.map(({ integration, summary, updatedAt }) => (
<Card key={integration.id} withBorder p="sm">
<Card key={integration.id} withBorder p="sm" bg="transparent" style={{ borderColor: neutralBorderColor }}>
<Stack gap="sm">
<Group justify="space-between" wrap="nowrap">
<Stack gap={0} miw={0}>
@@ -87,10 +90,20 @@ export default function NetworkControllerNetworkStatusWidget({
</Badge>
</Group>
<SimpleGrid cols={layout.columns} spacing="sm">
<Card p={layout.cardPadding} withBorder>
<Card
p={layout.cardPadding}
withBorder
bg="transparent"
style={{ borderColor: neutralBorderColor }}
>
<WifiVariant countGuests={summary.wifi.guests} countUsers={summary.wifi.users} />
</Card>
<Card p={layout.cardPadding} withBorder>
<Card
p={layout.cardPadding}
withBorder
bg="transparent"
style={{ borderColor: neutralBorderColor }}
>
<WiredVariant countGuests={summary.lan.guests} countUsers={summary.lan.users} />
</Card>
</SimpleGrid>
@@ -105,9 +118,15 @@ export default function NetworkControllerNetworkStatusWidget({
return (
<Stack p={layout.padding} h="100%" gap={0}>
{queryIndicators}
<SimpleGrid cols={layout.columns} h="100%" spacing="sm">
<SimpleGrid cols={layout.columns} spacing="sm" style={{ flex: 1, alignItems: "stretch" }}>
{layout.showWifi && (
<Card p={layout.cardPadding} withBorder={layout.withBorder}>
<Card
p={layout.cardPadding}
withBorder={layout.withBorder}
bg="transparent"
h="100%"
style={{ borderColor: neutralBorderColor }}
>
<WifiVariant
countGuests={countWifiGuests}
countUsers={countWifiUsers}
@@ -117,7 +136,13 @@ export default function NetworkControllerNetworkStatusWidget({
</Card>
)}
{layout.showWired && (
<Card p={layout.cardPadding} withBorder={layout.withBorder}>
<Card
p={layout.cardPadding}
withBorder={layout.withBorder}
bg="transparent"
h="100%"
style={{ borderColor: neutralBorderColor }}
>
<WiredVariant
countGuests={countLanGuests}
countUsers={countLanUsers}
@@ -10,11 +10,11 @@ export const StatRow = ({
compact?: boolean;
}) => {
return (
<Stack gap={0}>
<Text size={compact ? "xl" : "2xl"} fw={800} lh={1}>
<Stack gap={2} align="center" justify="center">
<Text size={compact ? "xl" : "2xl"} fw={800} lh={1} ta="center">
{value}
</Text>
<Text size={compact ? "sm" : "md"} c="dimmed">
<Text size={compact ? "sm" : "md"} c="dimmed" ta="center">
{label}
</Text>
</Stack>
@@ -1,4 +1,4 @@
import { Group, SimpleGrid, Text } from "@mantine/core";
import { Center, Group, SimpleGrid, Stack, Text } from "@mantine/core";
import { IconWifi } from "@tabler/icons-react";
import { useI18n } from "@homarr/translation/client";
@@ -18,17 +18,19 @@ export const WifiVariant = ({
}) => {
const t = useI18n("widget.networkControllerStatus.card");
return (
<>
<Group gap="xs" wrap="nowrap" mb={compact ? "xs" : "md"}>
<IconWifi size={24} />
<Stack h="100%" align="center" justify="center" gap={compact ? "sm" : "md"}>
<Group gap="xs" wrap="nowrap" justify="center">
<Center w={24} h={24}>
<IconWifi size={20} />
</Center>
<Text size={"md"} fw={"bold"}>
{t("variants.wifi.name")}
</Text>
</Group>
<SimpleGrid cols={horizontal ? 2 : 1} spacing={compact ? "sm" : "lg"}>
<SimpleGrid cols={horizontal ? 2 : 1} spacing={compact ? "sm" : "lg"} w="100%" maw={horizontal ? 360 : 220}>
<StatRow label={t("users.label")} value={countUsers} compact={compact} />
<StatRow label={t("guests.label")} value={countGuests} compact={compact} />
</SimpleGrid>
</>
</Stack>
);
};
@@ -1,4 +1,4 @@
import { Group, SimpleGrid, Text } from "@mantine/core";
import { Center, Group, SimpleGrid, Stack, Text } from "@mantine/core";
import { IconNetwork } from "@tabler/icons-react";
import { useI18n } from "@homarr/translation/client";
@@ -18,17 +18,19 @@ export const WiredVariant = ({
}) => {
const t = useI18n("widget.networkControllerStatus.card");
return (
<>
<Group gap="xs" wrap="nowrap" mb={compact ? "xs" : "md"}>
<IconNetwork size={24} />
<Stack h="100%" align="center" justify="center" gap={compact ? "sm" : "md"}>
<Group gap="xs" wrap="nowrap" justify="center">
<Center w={24} h={24}>
<IconNetwork size={20} />
</Center>
<Text size={"md"} fw={"bold"}>
{t("variants.wired.name")}
</Text>
</Group>
<SimpleGrid cols={horizontal ? 2 : 1} spacing={compact ? "sm" : "lg"}>
<SimpleGrid cols={horizontal ? 2 : 1} spacing={compact ? "sm" : "lg"} w="100%" maw={horizontal ? 360 : 220}>
<StatRow label={t("users.label")} value={countUsers} compact={compact} />
<StatRow label={t("guests.label")} value={countGuests} compact={compact} />
</SimpleGrid>
</>
</Stack>
);
};
@@ -83,7 +83,13 @@ export default function NetworkControllerSummaryWidget({
{queryIndicators}
<SimpleGrid cols={controllerColumns} spacing="sm">
{summaries.map(({ integration, summary, updatedAt }) => (
<Card key={integration.id} p="sm" withBorder>
<Card
key={integration.id}
p="sm"
withBorder
bg="transparent"
style={{ borderColor: neutralSurfaceBorder }}
>
<Stack gap="sm">
<Group justify="space-between" align="flex-start" wrap="nowrap">
<Stack gap={0} style={{ minWidth: 0 }}>
@@ -105,7 +111,13 @@ export default function NetworkControllerSummaryWidget({
</Group>
<SimpleGrid cols={matrixColumns} spacing="xs">
{getNetworkControllerMatrix(summary).map((section) => (
<Card key={section.key} p="xs" withBorder>
<Card
key={section.key}
p="xs"
withBorder
bg="transparent"
style={{ borderColor: neutralSurfaceBorder }}
>
<Stack gap="xs">
<Group justify="space-between" wrap="nowrap">
<Group gap={6} wrap="nowrap">
@@ -149,12 +161,18 @@ export default function NetworkControllerSummaryWidget({
}
return (
<ScrollArea h="100%" p={isDense ? "xs" : "sm"}>
<Stack h="100%" p={isDense ? "xs" : "sm"} gap={0} style={{ overflow: "hidden" }}>
{queryIndicators}
<SimpleGrid cols={columns} spacing="sm">
<SimpleGrid cols={columns} spacing="sm" style={{ flex: 1, minHeight: 0, alignContent: "center" }}>
{summaries.map(({ integration, summary }) => (
<Card key={integration.id} p={isDense ? "xs" : "sm"} withBorder={summaries.length > 1}>
<Stack gap="xs">
<Card
key={integration.id}
p={isDense ? "xs" : "sm"}
withBorder={summaries.length > 1}
bg="transparent"
style={{ borderColor: neutralSurfaceBorder }}
>
<Stack h="100%" gap={isDense ? 4 : 8} justify="center" align="center">
{summaries.length > 1 && (
<Group justify="space-between" wrap="nowrap">
<Text fw={600} size="sm" truncate="end">
@@ -165,7 +183,7 @@ export default function NetworkControllerSummaryWidget({
</Badge>
</Group>
)}
<List spacing={isDense ? 2 : "xs"} center size={isDense ? "sm" : undefined}>
<List spacing={isDense ? 2 : 4} center size={isDense ? "xs" : "sm"} w="fit-content" mx="auto">
<List.Item
icon={
<StatusIcon
@@ -211,7 +229,7 @@ export default function NetworkControllerSummaryWidget({
<StatusIcon
status={summary.lan.status}
label={statusLabels[getBinaryStatusKey(summary.lan.status)]}
size="var(--mantine-font-size-xl)"
size={isDense ? 16 : 20}
/>
}
>
@@ -239,7 +257,7 @@ export default function NetworkControllerSummaryWidget({
</Card>
))}
</SimpleGrid>
</ScrollArea>
</Stack>
);
}
@@ -262,6 +280,9 @@ const getMatrixSectionLabel = (
return t(`card.${key}` as never);
};
const neutralSurfaceBorder =
"rgb(from var(--mantine-color-default-border) r g b / calc(var(--opacity, 1) * 0.45))";
const StatusIcon = ({
status,
label,
+25 -3
View File
@@ -1,5 +1,6 @@
.tiptap hr {
border-top-style: double;
border-top-color: rgb(from var(--mantine-color-default-border) r g b / calc(var(--opacity, 1) * 0.45));
}
.tiptap ul[data-type="taskList"] {
@@ -20,6 +21,10 @@
outline: 3px solid rgba(0, 65, 198, 0.8);
}
.tiptap table :where(tr) {
background-color: transparent;
}
.tiptap table {
border-collapse: collapse;
margin: 0;
@@ -28,8 +33,8 @@
width: 100%;
}
.tiptap table td {
border-color: var(--mantine-color-gray-5) !important;
.tiptap table :where(th, td) {
border-color: rgb(from var(--mantine-color-default-border) r g b / calc(var(--opacity, 1) * 0.45)) !important;
border-width: 1px !important;
border-style: solid !important;
box-sizing: border-box;
@@ -39,7 +44,7 @@
vertical-align: top;
}
.tiptap table td > * {
.tiptap table :where(th, td) > * {
margin-bottom: 0;
}
@@ -66,6 +71,23 @@
.tiptap[contenteditable="false"].resize-cursor {
pointer-events: none;
}
.homarr-notebook-toolbar-group {
background-color: rgb(from var(--mantine-color-body) r g b / calc(var(--opacity, 1) * 0.12));
}
.homarr-notebook .tiptap :where(pre, blockquote) {
background-color: rgb(from var(--mantine-color-body) r g b / calc(var(--opacity, 1) * 0.12));
}
.homarr-notebook .tiptap :where(code, kbd) {
background-color: rgb(from var(--mantine-color-body) r g b / calc(var(--opacity, 1) * 0.12));
border-color: rgb(from var(--mantine-color-default-border) r g b / calc(var(--opacity, 1) * 0.45));
}
.homarr-notebook .tiptap :where(pre) :where(code) {
background-color: transparent;
}
.ProseMirror {
height: 100%;
+12 -12
View File
@@ -121,8 +121,6 @@ export function Notebook({
}, [options.allowReadOnlyCheck]);
const { primaryColor } = useMantineTheme();
const { colorScheme } = useMantineColorScheme();
const { mutateAsync, isPending: isSaving } = clientApi.widget.notebook.updateContent.useMutation({
scope: { id: `notebook-content:${boardId ?? "preview"}:${itemId ?? "preview"}` },
});
@@ -360,9 +358,9 @@ export function Notebook({
isEditing && !isSaving ? getHotkeyHandler([[hotkeys.saveNotebook, () => void handleEditToggle()]]) : undefined
}
editor={editor}
styles={(theme) => ({
styles={() => ({
root: {
backgroundColor: colorScheme === "dark" ? theme.colors.dark[6] : "white",
backgroundColor: "transparent",
border: "none",
borderRadius: "0.5rem",
display: "flex",
@@ -371,6 +369,8 @@ export function Notebook({
},
toolbar: {
backgroundColor: "transparent",
borderColor:
"rgb(from var(--mantine-color-default-border) r g b / calc(var(--opacity, 1) * 0.45))",
padding: "0.5rem",
},
content: {
@@ -390,7 +390,7 @@ export function Notebook({
overflowY: display.toolbarMaxHeight === undefined ? undefined : "auto",
}}
>
<RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup className="homarr-notebook-toolbar-group">
<RichTextEditor.Bold title={tControls("bold")} />
<RichTextEditor.Italic title={tControls("italic")} />
<RichTextEditor.Strikethrough title={tControls("strikethrough")} />
@@ -401,14 +401,14 @@ export function Notebook({
<RichTextEditor.ClearFormatting title={tControls("clear")} />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup className="homarr-notebook-toolbar-group">
<RichTextEditor.H1 title={tControls("heading", { level: "1" })} />
<RichTextEditor.H2 title={tControls("heading", { level: "2" })} />
<RichTextEditor.H3 title={tControls("heading", { level: "3" })} />
<RichTextEditor.H4 title={tControls("heading", { level: "4" })} />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup className="homarr-notebook-toolbar-group">
<RichTextEditor.AlignLeft
title={tControls("align", {
position: t("align.left"),
@@ -442,12 +442,12 @@ export function Notebook({
</RichTextEditor.Control>
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup className="homarr-notebook-toolbar-group">
<RichTextEditor.Blockquote title={tControls("blockquote")} />
<RichTextEditor.Hr title={tControls("horizontalLine")} />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup className="homarr-notebook-toolbar-group">
<RichTextEditor.BulletList title={tControls("bulletList")} />
<RichTextEditor.OrderedList title={tControls("orderedList")} />
<TaskListToggle />
@@ -461,13 +461,13 @@ export function Notebook({
)}
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup className="homarr-notebook-toolbar-group">
<RichTextEditor.Link title={tControls("link")} />
<RichTextEditor.Unlink title={tControls("unlink")} />
<EmbedImage />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup className="homarr-notebook-toolbar-group">
<TableToggle />
{editor?.isActive("table") && (
<>
@@ -483,7 +483,7 @@ export function Notebook({
)}
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup className="homarr-notebook-toolbar-group">
<RichTextEditor.Undo />
<RichTextEditor.Redo />
</RichTextEditor.ControlsGroup>
@@ -0,0 +1,5 @@
.card,
.row {
background-color: transparent;
border-color: rgb(from var(--mantine-color-default-border) r g b / calc(var(--opacity, 1) * 0.38));
}
@@ -14,6 +14,7 @@ import { getUsableWidgetQueryData } from "../common/query-state";
import { WidgetQueryErrorIndicator } from "../common/query-state-indicator";
import type { WidgetComponentProps } from "../definition";
import { getNotificationDisplay } from "./display";
import classes from "./component.module.css";
export default function NotificationsWidget({
options,
@@ -97,17 +98,13 @@ export default function NotificationsWidget({
href={href}
target={href ? "_blank" : undefined}
rel={href ? SAFE_NEW_TAB_REL : undefined}
className={columns > 1 ? classes.card : classes.row}
radius={board.itemRadius}
w="100%"
p={isRoomy ? "sm" : isDense ? 6 : "xs"}
bg={columns > 1 ? undefined : "transparent"}
style={{
color: "inherit",
textDecoration: "none",
borderBottom:
columns > 1
? undefined
: "1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4))",
}}
>
<Flex gap={isDense ? "xs" : "sm"} align="flex-start" w="100%">
@@ -16,7 +16,8 @@
gap: clamp(8px, 3cqi, 18px);
flex-shrink: 0;
padding-bottom: clamp(4px, 2cqi, 10px);
border-bottom: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4));
border-bottom: 1px solid
rgb(from var(--mantine-color-default-border) r g b / calc(var(--opacity, 1) * 0.45));
}
.heroExpanded {
@@ -97,7 +98,9 @@
gap: clamp(2px, 1.5cqi, 5px);
padding: clamp(4px, 2.5cqi, 10px);
border-radius: var(--mantine-radius-md);
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
border: 1px solid
rgb(from var(--mantine-color-default-border) r g b / calc(var(--opacity, 1) * 0.45));
background-color: transparent;
overflow: hidden;
}
@@ -120,7 +123,7 @@
.statValue {
font-size: clamp(14px, 5.5cqi, 24px);
font-weight: 700;
color: var(--mantine-primary-color-filled);
color: var(--mantine-color-text);
line-height: 1.1;
text-align: center;
}
@@ -35,7 +35,7 @@ export function RecentActivityList({
: session.mediaTitle;
return (
<Paper key={session.key} p="sm" radius="lg">
<Paper key={session.key} p="sm" radius="lg" bg="transparent">
<Group justify="space-between" wrap="nowrap">
<Group gap="xs" wrap="nowrap" style={{ overflow: "hidden" }}>
<Avatar src={session.user.avatarUrl} alt={session.user.username} radius="xl" size="sm" />
@@ -44,7 +44,7 @@ export function StatsBar({
function StatCard({ icon, label, value }: { icon: React.ReactNode; label: string; value: string | number }) {
return (
<Paper p="xs" radius="lg">
<Paper p="xs" radius="lg" bg="transparent">
<Group gap={4} wrap="nowrap">
{icon}
<Stack gap={0} style={{ overflow: "hidden" }}>
@@ -65,7 +65,7 @@ function StreamCard({
: null;
return (
<Paper p="xs" radius="lg" style={{ position: "relative", overflow: "hidden" }}>
<Paper p="xs" radius="lg" bg="transparent" style={{ position: "relative", overflow: "hidden" }}>
{stream.posterUrl && (
<Box
style={{
@@ -30,7 +30,7 @@ export function ViolationsList({
) : (
<Stack gap="xs">
{violations.map((violation) => (
<Paper key={violation.key} p="xs" radius="lg">
<Paper key={violation.key} p="xs" radius="lg" bg="transparent">
<Group justify="space-between" wrap="nowrap">
<Group gap="xs" wrap="nowrap" style={{ overflow: "hidden" }}>
<IconAlertTriangle
@@ -39,7 +39,7 @@
gap: clamp(5px, 2cqi, 8px);
padding: clamp(6px, 3cqi, 10px);
border-radius: var(--mantine-radius-sm);
background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
background-color: transparent;
}
.metricIcon {
@@ -75,7 +75,7 @@
.protocolCard {
min-width: 0;
padding: clamp(6px, 3cqi, 10px);
border: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4));
border: 1px solid rgb(from var(--mantine-color-default-border) r g b / calc(var(--opacity, 1) * 0.45));
border-radius: var(--mantine-radius-sm);
}
+17 -4
View File
@@ -35,6 +35,11 @@ const statusColors: Record<UpsStatus, string> = {
unknown: "gray",
};
const neutralSurfaceBackground =
"rgb(from var(--mantine-color-default-hover) r g b / calc(var(--opacity, 1) * 0.12))";
const neutralSurfaceBorder =
"rgb(from var(--mantine-color-default-border) r g b / calc(var(--opacity, 1) * 0.45))";
type UpsLayout = "mini" | "compact" | "full";
export default function UpsWidget({
@@ -183,7 +188,7 @@ function UpsDeviceCard({ summary, sourceName, options, layout, advanced, t }: Up
if (layout === "mini") {
return (
<Card p={6} radius="md">
<Card p={6} radius="md" bg="transparent">
<Group gap={4} wrap="nowrap" justify="center" mb={showRing ? 4 : 0}>
<Box
w={8}
@@ -211,6 +216,7 @@ function UpsDeviceCard({ summary, sourceName, options, layout, advanced, t }: Up
size={44}
thickness={4}
roundCaps
rootColor={neutralSurfaceBorder}
sections={[{ value: clampPercent(summary.batteryCharge), color: getChargeColor(summary.batteryCharge) }]}
label={
<Text ta="center" size="10px" fw={700}>
@@ -226,13 +232,14 @@ function UpsDeviceCard({ summary, sourceName, options, layout, advanced, t }: Up
if (layout === "compact") {
return (
<Card p={6} radius="md">
<Card p={6} radius="md" bg="transparent">
<Group gap={8} wrap="nowrap" align="center">
{showRing && summary.batteryCharge !== null && (
<RingProgress
size={40}
thickness={4}
roundCaps
rootColor={neutralSurfaceBorder}
sections={[{ value: clampPercent(summary.batteryCharge), color: getChargeColor(summary.batteryCharge) }]}
label={
<Text ta="center" size="9px" fw={700}>
@@ -258,7 +265,7 @@ function UpsDeviceCard({ summary, sourceName, options, layout, advanced, t }: Up
}
return (
<Card p="xs" radius="md">
<Card p="xs" radius="md" bg="transparent">
<Group justify="space-between" wrap="nowrap" mb={4}>
<Stack gap={0} style={{ minWidth: 0 }}>
<Text fw={600} size="sm" truncate>
@@ -279,6 +286,7 @@ function UpsDeviceCard({ summary, sourceName, options, layout, advanced, t }: Up
size={68}
thickness={6}
roundCaps
rootColor={neutralSurfaceBorder}
sections={[{ value: clampPercent(summary.batteryCharge), color: getChargeColor(summary.batteryCharge) }]}
label={
<Text ta="center" size="xs" fw={700}>
@@ -301,7 +309,12 @@ function UpsDeviceCard({ summary, sourceName, options, layout, advanced, t }: Up
</Text>
<Text size="xs">{Math.round(summary.load)}%</Text>
</Group>
<Progress value={clampPercent(summary.load)} size="sm" color={getLoadColor(summary.load)} />
<Progress
value={clampPercent(summary.load)}
size="sm"
color={getLoadColor(summary.load)}
styles={{ root: { backgroundColor: neutralSurfaceBackground } }}
/>
</Box>
)}
@@ -16,7 +16,8 @@
gap: clamp(8px, 3cqi, 18px);
flex-shrink: 0;
padding-bottom: clamp(4px, 2cqi, 10px);
border-bottom: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4));
border-bottom: 1px solid
rgb(from var(--mantine-color-default-border) r g b / calc(var(--opacity, 1) * 0.45));
}
.heroExpanded {
@@ -83,7 +84,9 @@
gap: clamp(2px, 1.5cqi, 5px);
padding: clamp(4px, 2.5cqi, 10px);
border-radius: var(--mantine-radius-md);
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
border: 1px solid
rgb(from var(--mantine-color-default-border) r g b / calc(var(--opacity, 1) * 0.45));
background-color: transparent;
overflow: hidden;
}
+11 -1
View File
@@ -19,6 +19,9 @@ import { getStatusColor, RUNNING_STATUS } from "./helpers";
export type VpnInfo = RouterOutputs["widget"]["vpn"]["getSummaries"][number]["summary"];
const neutralBorderColor =
"rgb(from var(--mantine-color-default-border) r g b / calc(var(--opacity, 1) * 0.45))";
export function VpnIntegrationCard({
vpn,
integrationName,
@@ -53,7 +56,14 @@ export function VpnIntegrationCard({
if (compact) {
return (
<Card withBorder radius={board.itemRadius} p={dense ? 6 : "xs"} w="100%">
<Card
withBorder
radius={board.itemRadius}
p={dense ? 6 : "xs"}
w="100%"
bg="transparent"
style={{ borderColor: neutralBorderColor }}
>
{content}
</Card>
);
@@ -1,7 +1,3 @@
[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);
.card {
background-color: transparent;
}
+8 -2
View File
@@ -206,14 +206,20 @@ const UpdateCard = ({
? t("updateAvailable")
: buildVersionText(truncateVersion(update.currentVersion), truncateVersion(update.newVersion));
const versionBadge = versionText ? (
<Badge size="xs" variant="light" color="gray" style={{ whiteSpace: "nowrap" }}>
<Badge size="xs" variant="subtle" color="gray" style={{ whiteSpace: "nowrap" }}>
{versionText}
</Badge>
) : null;
const showVersionTooltip = versionText !== null && !isDigestUpdate && versionText !== fullVersionText;
return (
<Card className={combineClasses(className)} radius={radius} p="xs" style={{ overflow: "visible" }}>
<Card
className={combineClasses(className)}
radius={radius}
p="xs"
bg="transparent"
style={{ overflow: "visible" }}
>
<Group justify="space-between" wrap="nowrap" gap="xs" miw={0}>
<Text size="xs" fw={500} lineClamp={1} miw={0}>
{update.name}