Compare commits

..
Author SHA1 Message Date
ajnart dcc9661790 fix: remove stale weather descriptions 2026-08-22 16:51:27 +02:00
ajnart b7a390451d fix: use semantic bookmark title sizes 2026-08-22 16:50:48 +02:00
ajnart b7d72d2106 feat: add bookmark card spacing 2026-08-22 16:37:39 +02:00
ajnart 471aa0a49f fix: address bookmarks review feedback 2026-08-22 16:13:48 +02:00
ajnart 09ecb3f0b0 feat: rework bookmarks widget 2026-08-22 16:11:14 +02:00
27 changed files with 102 additions and 174 deletions

No files matched your search

+3 -4
View File
@@ -27,10 +27,9 @@ opening it. Containers include their visible widgets and app icons instead of ap
Names of boards must be unique because this will be used in the URL to load the desired board.
Press `Shift+C`, or select **Switch board** from the user menu, to open the board switcher. It shows structural
previews of every board you can access, with your current board at the end. Home-board markers and creator avatars
match the board management view. Use the search field at the top, or simply start typing, to filter by name. Use the
arrow keys to move through the results and press `Enter` to open the selected board.
Press `Shift+C`, or select **Switch board** from the user menu, to open the board switcher. It shows
structural previews of the other boards you can access. Start typing to filter by name, use the arrow keys to move
through the results, and press `Enter` to open the selected board.
## Create a new board
@@ -1,6 +1,5 @@
.canvas {
width: 100%;
min-width: 0;
min-height: 8rem;
aspect-ratio: 16 / 7;
overflow: hidden;
color: var(--mantine-color-dimmed);
@@ -84,3 +83,9 @@
gap: rem(1px);
overflow: hidden;
}
@media (max-width: $mantine-breakpoint-xs) {
.canvas {
min-height: 7rem;
}
}
@@ -11,12 +11,11 @@ import classes from "./board-layout-thumbnail.module.css";
interface BoardLayoutThumbnailProps {
preview: BoardPreviewData | null;
label: string;
fitFullLayout?: boolean;
}
const maxThumbnailRows = 12;
export const BoardLayoutThumbnail = ({ preview, label, fitFullLayout = false }: BoardLayoutThumbnailProps) => {
export const BoardLayoutThumbnail = ({ preview, label }: BoardLayoutThumbnailProps) => {
const layout = preview?.layouts.find((candidate) => candidate.role === "base") ?? preview?.layouts.at(0);
if (!preview || !layout) {
return (
@@ -27,7 +26,6 @@ export const BoardLayoutThumbnail = ({ preview, label, fitFullLayout = false }:
}
const elements = projectBoardLayout(preview, layout, layout);
const thumbnailRowLimit = fitFullLayout ? Number.POSITIVE_INFINITY : maxThumbnailRows;
const roots = preview.sections
.filter((section) => section.kind === "empty")
.toSorted((first, second) => (first.xOffset ?? 0) - (second.xOffset ?? 0) || first.id.localeCompare(second.id));
@@ -67,12 +65,12 @@ export const BoardLayoutThumbnail = ({ preview, label, fitFullLayout = false }:
element.xOffset >= 0 &&
element.xOffset < columnCount &&
element.yOffset >= 0 &&
element.yOffset < thumbnailRowLimit &&
element.yOffset < maxThumbnailRows &&
element.width > 0 &&
element.height > 0,
);
const rowCount = Math.min(
thumbnailRowLimit,
maxThumbnailRows,
Math.max(4, ...laneElements.map((element) => element.yOffset + element.height)),
);
@@ -1,26 +1,7 @@
.modalContent {
overflow: hidden;
border: rem(1px) solid color-mix(in srgb, var(--mantine-color-default-border) 70%, transparent);
border-radius: rem(24px);
background: color-mix(in srgb, var(--mantine-color-body) 58%, transparent);
box-shadow: 0 rem(18px) rem(56px) rgb(0 0 0 / 22%);
backdrop-filter: blur(rem(20px)) saturate(140%);
}
.modalBody {
padding: var(--mantine-spacing-sm);
}
.content {
outline: none;
}
.searchInput {
border-color: color-mix(in srgb, var(--mantine-color-default-border) 72%, transparent);
background: color-mix(in srgb, var(--mantine-color-body) 54%, transparent);
backdrop-filter: blur(rem(8px));
}
.option {
min-width: 0;
border-radius: var(--mantine-radius-md);
@@ -30,12 +11,6 @@
.card {
height: 100%;
overflow: hidden;
box-shadow: var(--mantine-shadow-sm);
}
.metadataBadge {
width: rem(22px);
padding-inline: 0;
}
.option:hover .card,
@@ -3,31 +3,29 @@
import type { KeyboardEvent, ReactNode, RefObject } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
Badge,
Card,
Center,
Group,
Kbd,
Loader,
Modal,
ScrollArea,
SimpleGrid,
Stack,
Text,
TextInput,
ThemeIcon,
Tooltip,
UnstyledButton,
useMatches,
VisuallyHidden,
} from "@mantine/core";
import { useHotkeys } from "@mantine/hooks";
import { IconDeviceMobile, IconHomeFilled, IconLayoutBoard, IconSearch } from "@tabler/icons-react";
import { IconArrowDown, IconArrowRight, IconLayoutBoard, IconReplace } from "@tabler/icons-react";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { useOptionalBoard } from "@homarr/boards/context";
import { useI18n } from "@homarr/translation/client";
import { Link, UserAvatar } from "@homarr/ui";
import { Link } from "@homarr/ui";
import { BoardLayoutThumbnail } from "~/components/board/board-layout-thumbnail";
@@ -41,7 +39,6 @@ interface BoardSwitcherProps {
export const BoardSwitcher = ({ children }: BoardSwitcherProps) => {
const t = useI18n("board.action.switcher");
const manageBoardsT = useI18n("management.page.board");
const currentBoard = useOptionalBoard();
const [isOpen, setIsOpen] = useState(false);
const [search, setSearch] = useState("");
@@ -53,27 +50,20 @@ export const BoardSwitcher = ({ children }: BoardSwitcherProps) => {
data: boards = [],
isPending,
isError,
} = clientApi.board.getManageOverview.useQuery(
{ fullPreview: true },
{
enabled: isOpen,
},
);
} = clientApi.board.getManageOverview.useQuery(undefined, {
enabled: isOpen,
});
const switcherBoards = useMemo(
() => [
...boards.filter((board) => board.id !== currentBoard?.id),
...boards.filter((board) => board.id === currentBoard?.id),
],
const availableBoards = useMemo(
() => boards.filter((board) => board.id !== currentBoard?.id),
[boards, currentBoard?.id],
);
const filteredBoards = useMemo(() => {
const normalizedSearch = search.trim().toLocaleLowerCase();
if (normalizedSearch.length === 0) return switcherBoards;
return switcherBoards.filter((board) => board.name.toLocaleLowerCase().includes(normalizedSearch));
}, [search, switcherBoards]);
if (normalizedSearch.length === 0) return availableBoards;
return availableBoards.filter((board) => board.name.toLocaleLowerCase().includes(normalizedSearch));
}, [availableBoards, search]);
const columnCount = Math.max(1, Math.min(filteredBoards.length, responsiveColumnCount));
const modalColumnCount = Math.max(1, Math.min(switcherBoards.length, responsiveColumnCount));
const openSwitcher = useCallback(() => {
setIsOpen(true);
@@ -83,7 +73,7 @@ export const BoardSwitcher = ({ children }: BoardSwitcherProps) => {
setSearch("");
setActiveIndex(0);
}, []);
const preloadBoards = () => void utils.board.getManageOverview.prefetch({ fullPreview: true });
const preloadBoards = () => void utils.board.getManageOverview.prefetch();
useHotkeys([[boardSwitcherHotkey, openSwitcher, { preventDefault: true }]]);
@@ -98,17 +88,14 @@ export const BoardSwitcher = ({ children }: BoardSwitcherProps) => {
}, [activeIndex, filteredBoards.length, isOpen]);
const handleModalKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
const isSearchInput =
event.target instanceof HTMLInputElement && event.target.dataset.boardSwitcherSearch !== undefined;
if (!isSearchInput && event.key === "Backspace") {
if (event.key === "Backspace") {
event.preventDefault();
setSearch((current) => current.slice(0, -1));
setActiveIndex(0);
return;
}
if (!isSearchInput && event.key.length === 1 && !event.altKey && !event.ctrlKey && !event.metaKey) {
if (event.key.length === 1 && !event.altKey && !event.ctrlKey && !event.metaKey) {
event.preventDefault();
setSearch((current) => `${current}${event.key}`);
setActiveIndex(0);
@@ -154,7 +141,7 @@ export const BoardSwitcher = ({ children }: BoardSwitcherProps) => {
};
const results = getBoardSwitcherResults({
availableBoardCount: switcherBoards.length,
availableBoardCount: availableBoards.length,
filteredBoards,
isError,
isPending,
@@ -165,7 +152,6 @@ export const BoardSwitcher = ({ children }: BoardSwitcherProps) => {
setActiveIndex,
optionRefs,
columnCount,
manageBoardsT,
});
return (
@@ -175,40 +161,54 @@ export const BoardSwitcher = ({ children }: BoardSwitcherProps) => {
<Modal
opened={isOpen}
onClose={closeSwitcher}
withCloseButton={false}
yOffset="15vh"
aria-label={t("title")}
size={`${modalColumnCount * 15 + Math.max(0, modalColumnCount - 1) * 0.75}rem`}
overlayProps={{ backgroundOpacity: 0, blur: 2 }}
title={
<Group gap="sm" wrap="nowrap">
<ThemeIcon variant="light" radius="md">
<IconReplace size={18} stroke={1.5} />
</ThemeIcon>
<Text fw={700}>{t("title")}</Text>
</Group>
}
size={1280}
yOffset={32}
overlayProps={{ backgroundOpacity: 0.18, blur: 2 }}
transitionProps={{ transition: "fade", duration: 100, timingFunction: "ease" }}
classNames={{ content: classes.modalContent, body: classes.modalBody }}
>
<Stack
data-autofocus
tabIndex={-1}
gap="xs"
gap="md"
onKeyDown={handleModalKeyDown}
className={classes.content}
aria-describedby="board-switcher-instructions"
>
<VisuallyHidden id="board-switcher-instructions">{t("keyboard.instructions")}</VisuallyHidden>
<TextInput
data-autofocus
data-board-switcher-search
value={search}
onChange={(event) => {
setSearch(event.currentTarget.value);
setActiveIndex(0);
}}
leftSection={<IconSearch size="1rem" stroke={1.6} />}
placeholder={t("title")}
aria-label={t("title")}
aria-controls="board-switcher-results"
radius="xl"
classNames={{ input: classes.searchInput }}
/>
<ScrollArea.Autosize mah="min(80vh, 42rem)" type="never">
<ScrollArea.Autosize mah={640} type="auto" offsetScrollbars>
{results}
</ScrollArea.Autosize>
<Group justify={currentBoard ? "space-between" : "flex-end"} gap="xs" visibleFrom="sm">
{currentBoard && (
<Text size="xs" c="dimmed">
{t("currentHidden", { name: currentBoard.name })}
</Text>
)}
<Group gap="xs">
<Kbd size="xs">
<IconArrowRight size={12} />
</Kbd>
<Kbd size="xs">
<IconArrowDown size={12} />
</Kbd>
<Text size="xs" c="dimmed">
{t("keyboard.navigate")}
</Text>
<Kbd size="xs">Enter</Kbd>
<Text size="xs" c="dimmed">
{t("keyboard.open")}
</Text>
</Group>
</Group>
</Stack>
</Modal>
</>
@@ -227,7 +227,6 @@ interface BoardSwitcherResultsProps {
setActiveIndex: (index: number) => void;
optionRefs: RefObject<Array<HTMLAnchorElement | null>>;
columnCount: number;
manageBoardsT: ReturnType<typeof useI18n<"management.page.board">>;
}
const getBoardSwitcherResults = ({
@@ -242,7 +241,6 @@ const getBoardSwitcherResults = ({
setActiveIndex,
optionRefs,
columnCount,
manageBoardsT,
}: BoardSwitcherResultsProps) => {
if (isPending) {
return (
@@ -289,7 +287,7 @@ const getBoardSwitcherResults = ({
}
return (
<SimpleGrid id="board-switcher-results" aria-label={t("results")} cols={columnCount} spacing="xs" p={2}>
<SimpleGrid id="board-switcher-results" aria-label={t("results")} cols={columnCount} spacing="md" p={4}>
{filteredBoards.map((board, index) => (
<UnstyledButton
key={board.id}
@@ -298,7 +296,7 @@ const getBoardSwitcherResults = ({
}}
className={classes.option}
w="100%"
maw={240}
maw={320}
mx="auto"
component={Link}
href={`/boards/${encodeURIComponent(board.name)}`}
@@ -308,50 +306,21 @@ const getBoardSwitcherResults = ({
onFocus={() => setActiveIndex(index)}
onPointerEnter={() => setActiveIndex(index)}
>
<Card padding={0} withBorder radius="lg" className={classes.card}>
<Card padding={0} withBorder radius="md" className={classes.card}>
<Card.Section>
<BoardLayoutThumbnail
preview={board.preview}
label={t("preview", { name: board.name, count: String(board.preview?.items.length ?? 0) })}
fitFullLayout
/>
</Card.Section>
<Card.Section withBorder px="sm" py={6}>
<Group justify="space-between" gap="xs" wrap="nowrap">
<Text fw={600} size="sm" truncate>
<Card.Section withBorder p="sm">
<Group gap="sm" wrap="nowrap">
<ThemeIcon variant="light" radius="md" size="md">
<IconLayoutBoard size={16} stroke={1.5} />
</ThemeIcon>
<Text fw={650} truncate>
{board.name}
</Text>
<Group gap={4} wrap="nowrap">
{board.isHome && (
<Tooltip label={manageBoardsT("action.setHomeBoard.badge.tooltip")}>
<Badge
className={classes.metadataBadge}
color="yellow"
variant="light"
aria-label={manageBoardsT("action.setHomeBoard.badge.label")}
>
<IconHomeFilled size="0.7rem" />
</Badge>
</Tooltip>
)}
{board.isMobileHome && (
<Tooltip label={manageBoardsT("action.setMobileHomeBoard.badge.tooltip")}>
<Badge
className={classes.metadataBadge}
color="yellow"
variant="light"
aria-label={manageBoardsT("action.setMobileHomeBoard.badge.label")}
>
<IconDeviceMobile size="0.7rem" />
</Badge>
</Tooltip>
)}
{board.creator && (
<Tooltip label={board.creator.name ?? manageBoardsT("preview.unknownCreator")}>
<UserAvatar user={board.creator} size="xs" />
</Tooltip>
)}
</Group>
</Group>
</Card.Section>
</Card>
+4 -5
View File
@@ -86,7 +86,6 @@ interface BoardItemPlacementRectangle {
const boardItemPlacementTails = new Map<string, Promise<void>>();
const maxManageOverviewPreviewRows = 12;
const manageOverviewInputSchema = z.object({ fullPreview: z.boolean().default(false) }).optional();
const serializeBoardItemPlacementAsync = async <T>(boardId: string, operation: () => Promise<T>) => {
const previous = boardItemPlacementTails.get(boardId) ?? Promise.resolve();
@@ -226,7 +225,7 @@ export const boardRouter = createTRPCRouter({
isMobileHome: currentUser?.mobileHomeBoardId === board.id,
}));
}),
getManageOverview: publicProcedure.input(manageOverviewInputSchema).query(async ({ ctx, input }) => {
getManageOverview: publicProcedure.query(async ({ ctx }) => {
const userId = ctx.session?.user.id;
const { boardIds, currentUser, groupMemberships } = await getBoardAccessContextAsync(ctx.db, userId);
const groupPermissionWhere = getBoardGroupPermissionWhere(groupMemberships);
@@ -307,7 +306,7 @@ export const boardRouter = createTRPCRouter({
inArray(itemLayouts.sectionId, rootSectionIds),
gte(itemLayouts.xOffset, 0),
gte(itemLayouts.yOffset, 0),
input?.fullPreview ? undefined : lt(itemLayouts.yOffset, maxManageOverviewPreviewRows),
lt(itemLayouts.yOffset, maxManageOverviewPreviewRows),
gt(itemLayouts.width, 0),
gt(itemLayouts.height, 0),
),
@@ -332,7 +331,7 @@ export const boardRouter = createTRPCRouter({
inArray(sectionLayouts.parentSectionId, rootSectionIds),
gte(sectionLayouts.xOffset, 0),
gte(sectionLayouts.yOffset, 0),
input?.fullPreview ? undefined : lt(sectionLayouts.yOffset, maxManageOverviewPreviewRows),
lt(sectionLayouts.yOffset, maxManageOverviewPreviewRows),
gt(sectionLayouts.width, 0),
gt(sectionLayouts.height, 0),
),
@@ -367,7 +366,7 @@ export const boardRouter = createTRPCRouter({
inArray(itemLayouts.sectionId, previewContainerSectionIds),
gte(itemLayouts.xOffset, 0),
gte(itemLayouts.yOffset, 0),
input?.fullPreview ? undefined : lt(itemLayouts.yOffset, maxManageOverviewPreviewRows),
lt(itemLayouts.yOffset, maxManageOverviewPreviewRows),
gt(itemLayouts.width, 0),
gt(itemLayouts.height, 0),
),
+1 -5
View File
@@ -337,7 +337,7 @@ describe("getManageOverview", () => {
expect(overview.map((board) => board.id)).not.toContain(hiddenBoardId);
});
test("returns compact previews by default and full previews on request", async () => {
test("returns one compact base-layout preview for each accessible board", async () => {
const db = createDb();
const caller = boardRouter.createCaller({ db, deviceType: undefined, session: defaultSession });
const boardId = createId();
@@ -487,10 +487,6 @@ describe("getManageOverview", () => {
expect(result[0]?.preview?.layouts).toHaveLength(1);
expect(result[0]?.preview?.items.map((item) => item.id)).toEqual([itemId, nestedItemId]);
expect(result[0]?.preview?.sections).toHaveLength(2);
const fullResult = await caller.getManageOverview({ fullPreview: true });
expect(fullResult[0]?.preview?.items.map((item) => item.id)).toEqual([itemId, offscreenItemId, nestedItemId]);
});
test("includes app and widget icons in compact previews", async () => {
+1 -1
View File
@@ -2,7 +2,7 @@ import type { MantineProviderProps } from "@mantine/core";
import { theme } from "./theme";
export { scrollAreaDefaultProps, theme } from "./theme";
export { theme } from "./theme";
export { modalSizeForm, modalSizeSelect } from "./theme/modal";
export * from "./components";
-13
View File
@@ -2,13 +2,6 @@ import { createTheme, rem } from "@mantine/core";
import { modalComponent } from "./theme/modal";
export const scrollAreaDefaultProps = {
type: "hover",
scrollbarSize: 2,
scrollHideDelay: 500,
offsetScrollbars: true,
} as const;
export const theme = createTheme({
primaryColor: "red",
autoContrast: true,
@@ -103,12 +96,6 @@ export const theme = createTheme({
style: { borderRadius: 5 },
},
},
ScrollArea: {
defaultProps: scrollAreaDefaultProps,
},
ScrollAreaAutosize: {
defaultProps: scrollAreaDefaultProps,
},
LoadingOverlay: {
defaultProps: {
zIndex: 1000,
@@ -324,7 +324,7 @@ export function JsonPathTreePicker({
</Combobox.Header>
)}
<Combobox.Options>
<ScrollArea.Autosize mah={420}>
<ScrollArea.Autosize mah={420} type="scroll">
{!hasData && <Combobox.Empty>{noDataHint}</Combobox.Empty>}
{hasData && visibleNodes.length === 0 && <Combobox.Empty>No matching paths</Combobox.Empty>}
{visibleNodes.map((node) => (
@@ -177,7 +177,7 @@ const AdvancedAirQuality = ({ airQuality, options, width }: AirQualityViewProps
];
return (
<ScrollArea h="100%" w="100%">
<ScrollArea h="100%" w="100%" type="auto" offsetScrollbars scrollbarSize={6}>
<Stack gap="md" p="md">
<Group justify="space-between" align="flex-start" wrap="wrap">
<Stack gap={2}>
@@ -163,7 +163,7 @@ export const BookmarkAddButton: SortableItemListInput<BookmarkSelectionItem, str
</Combobox.DropdownTarget>
<Combobox.Dropdown>
<ScrollArea.Autosize mah={280}>
<ScrollArea.Autosize mah={280} type="auto">
<Combobox.Options>
{pendingUrl && !selectedValues.has(pendingUrl.id) ? (
<Combobox.Option value={createOptionValue}>
@@ -148,7 +148,11 @@ export default function BookmarksWidget({
</Center>
) : (
<ScrollArea
type="hover"
scrollbars={plan.horizontalScroll ? "x" : "y"}
scrollbarSize={2}
scrollHideDelay={500}
offsetScrollbars
style={{ flex: 1, minHeight: 0 }}
>
<Box miw="100%" mih="100%" pb={2}>
@@ -56,6 +56,7 @@ export const CalendarEventList = ({
return (
<ScrollArea
offsetScrollbars
pt={5}
w="100%"
h={fillHeight ? "100%" : undefined}
@@ -68,6 +68,9 @@ export const AdvancedClockView = ({
<ScrollArea
h="100%"
w="100%"
type="auto"
offsetScrollbars
scrollbarSize={6}
aria-label={t("worldClock.advancedLabel")}
>
<Stack gap="md" p={{ base: "sm", sm: "md" }}>
@@ -22,16 +22,6 @@
background-color: color-mix(in srgb, var(--mantine-color-default-hover) 40%, transparent) !important;
}
.homarr-data-table .mantine-datatable-scroll-area-scrollbar[data-state="visible"]:hover {
@mixin light {
background-color: var(--mantine-color-gray-0);
}
@mixin dark {
background-color: var(--mantine-color-dark-8);
}
}
.homarr-data-table th {
padding: 0;
white-space: nowrap;
@@ -6,8 +6,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import type { DataTableColumn, DataTableProps } from "mantine-datatable";
import { DataTable } from "mantine-datatable";
import { scrollAreaDefaultProps } from "@homarr/ui";
const fallbackColumnWidth = 60;
const getColumnWidth = (width: string | number | undefined): number => {
@@ -155,7 +153,7 @@ export function HomarrDataTable<T>({
resizable: !isEditMode,
cellsStyle: defaultColumnProps?.cellsStyle ?? (() => ({ padding: cellPadding })),
}}
scrollAreaProps={{ ...scrollAreaDefaultProps, ...scrollAreaProps }}
scrollAreaProps={{ type: "auto", scrollbarSize: 6, ...scrollAreaProps }}
/>
);
}
+1 -1
View File
@@ -184,7 +184,7 @@ export default function CountdownWidget({
onNotificationsChange={setNotifications}
/>
<ScrollArea style={{ flex: 1, minHeight: 0 }}>
<ScrollArea type="auto" style={{ flex: 1, minHeight: 0 }}>
<Stack gap="md" pb="xs">
<EventSection
title={t("upcoming")}
@@ -104,7 +104,7 @@ export default function DnsHoleSummaryWidget({
)}
</SimpleGrid>
{showSourceStatuses && (
<ScrollArea px="xs" pb="xs">
<ScrollArea type="auto" scrollbarSize={4} px="xs" pb="xs">
<Group gap="xs" wrap="nowrap">
{successfulSummaries.map(({ integration, summary }) => (
<Badge
@@ -228,7 +228,7 @@ function Carousel({
)}
</Box>
{advanced && (
<ScrollArea px="xs" py={6}>
<ScrollArea type="never" scrollbarSize={0} px="xs" py={6}>
<Group gap={6} wrap="nowrap">
{assets.map((asset, index) => (
<UnstyledButton
@@ -126,6 +126,7 @@ export default function MediaMissingWidget({
) => (
<ScrollArea
h="100%"
scrollbarSize={4}
startScrollPosition={tabScrollPositionsRef.current[tab]}
onScrollPositionChange={(position) => {
tabScrollPositionsRef.current[tab] = position;
@@ -61,6 +61,7 @@ export default function MediaServerWidget({
)}
<ScrollArea
className="mediaRequests-list-scrollArea"
scrollbarSize="md"
style={{ flex: 1, minHeight: 0, pointerEvents: isEditMode ? "none" : undefined }}
>
<Stack className="mediaRequests-list-list" gap="xs" p="sm">
@@ -500,6 +500,7 @@ export function Notebook({
<ScrollArea
mih="4rem"
offsetScrollbars
pl={12}
pt={12}
styles={{
+1 -1
View File
@@ -387,7 +387,7 @@ const AdvancedTimer = ({
const t = useI18n("widget.timer");
const phaseLabel = getTimerPhaseLabel(runtime, t);
return (
<ScrollArea h="100%" w="100%">
<ScrollArea h="100%" w="100%" type="auto" offsetScrollbars scrollbarSize={6}>
<Stack gap="md" p="md">
<Paper
withBorder
@@ -5,6 +5,7 @@
}
.chartSurface :global(.mantine-ScrollArea-viewport),
.chartSurface :global(.mantine-ScrollArea-scrollbar),
.chartSurface :global(.recharts-wrapper),
.chartSurface :global(.recharts-surface) {
background-color: transparent !important;
+1 -1
View File
@@ -46,7 +46,7 @@ export default function VpnWidget({ options, integrationIds, width, height }: Wi
}
return (
<ScrollArea className="scroll-area-w100" w="100%" h="100%" p="xs">
<ScrollArea className="scroll-area-w100" w="100%" h="100%" offsetScrollbars p="xs">
<SimpleGrid cols={width >= 640 ? 2 : 1} spacing="sm">
{integrations.map((result) => (
<Stack key={result.integration.id} gap={4}>
+1 -1
View File
@@ -143,7 +143,7 @@ export const AdvancedWeather = ({ height, options, weather, width }: AdvancedWea
const ticks = hourly.filter((_, index) => index % layout.hourlyTickStep === 0).map((hour) => hour.observedAt);
return (
<ScrollArea h="100%" w="100%">
<ScrollArea h="100%" w="100%" type="auto" offsetScrollbars scrollbarSize={6}>
<Stack className={classes.advancedRoot} gap="md" p="md">
<Group justify="space-between" align="flex-start" gap="md" wrap="wrap">
<Group gap="md" wrap="nowrap">