fix(bookmarks): improve responsive styles (#2536)

* fix(bookmarks): improve responsive styles

* fix: typecheck issue
This commit is contained in:
Meier Lukas
2025-03-08 20:54:12 +01:00
committed by GitHub
parent ac5fc38ec0
commit df249a0173
4 changed files with 105 additions and 64 deletions

View File

@@ -38,6 +38,7 @@ const optionMapping: OptionMapping = {
return mappedLayouts[oldOptions.layout]; return mappedLayouts[oldOptions.layout];
}, },
hideTitle: () => undefined,
hideIcon: (oldOptions) => oldOptions.items.some((item) => item.hideIcon), hideIcon: (oldOptions) => oldOptions.items.some((item) => item.hideIcon),
hideHostname: (oldOptions) => oldOptions.items.some((item) => item.hideHostname), hideHostname: (oldOptions) => oldOptions.items.some((item) => item.hideHostname),
openNewTab: (oldOptions) => oldOptions.items.some((item) => item.openNewTab), openNewTab: (oldOptions) => oldOptions.items.some((item) => item.openNewTab),

View File

@@ -1127,9 +1127,15 @@
}, },
"grid": { "grid": {
"label": "Grid" "label": "Grid"
},
"gridHorizontal": {
"label": "Grid horizontal"
} }
} }
}, },
"hideTitle": {
"label": "Hide title"
},
"hideIcon": { "hideIcon": {
"label": "Hide icons" "label": "Hide icons"
}, },

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { Anchor, Box, Card, Divider, Flex, Group, Stack, Text, Title, UnstyledButton } from "@mantine/core"; import { Anchor, Card, Flex, Group, Stack, Text, Title, UnstyledButton } from "@mantine/core";
import combineClasses from "clsx"; import combineClasses from "clsx";
import type { RouterOutputs } from "@homarr/api"; import type { RouterOutputs } from "@homarr/api";
@@ -12,7 +12,7 @@ import { MaskedOrNormalImage } from "@homarr/ui";
import type { WidgetComponentProps } from "../definition"; import type { WidgetComponentProps } from "../definition";
import classes from "./bookmark.module.css"; import classes from "./bookmark.module.css";
export default function BookmarksWidget({ options, width, height, itemId }: WidgetComponentProps<"bookmarks">) { export default function BookmarksWidget({ options, itemId }: WidgetComponentProps<"bookmarks">) {
const board = useRequiredBoard(); const board = useRequiredBoard();
const [data] = clientApi.app.byIds.useSuspenseQuery(options.items, { const [data] = clientApi.app.byIds.useSuspenseQuery(options.items, {
select(data) { select(data) {
@@ -48,21 +48,22 @@ export default function BookmarksWidget({ options, width, height, itemId }: Widg
{options.title} {options.title}
</Title> </Title>
)} )}
{options.layout === "grid" && ( {(options.layout === "grid" || options.layout === "gridHorizontal") && (
<GridLayout <GridLayout
data={data} data={data}
width={width} itemDirection={options.layout === "gridHorizontal" ? "horizontal" : "vertical"}
height={height} hideTitle={options.hideTitle}
hideIcon={options.hideIcon} hideIcon={options.hideIcon}
hideHostname={options.hideHostname} hideHostname={options.hideHostname}
openNewTab={options.openNewTab} openNewTab={options.openNewTab}
hasIconColor={board.iconColor !== null} hasIconColor={board.iconColor !== null}
/> />
)} )}
{options.layout !== "grid" && ( {options.layout !== "grid" && options.layout !== "gridHorizontal" && (
<FlexLayout <FlexLayout
data={data} data={data}
direction={options.layout} direction={options.layout}
hideTitle={options.hideTitle}
hideIcon={options.hideIcon} hideIcon={options.hideIcon}
hideHostname={options.hideHostname} hideHostname={options.hideHostname}
openNewTab={options.openNewTab} openNewTab={options.openNewTab}
@@ -76,23 +77,27 @@ export default function BookmarksWidget({ options, width, height, itemId }: Widg
interface FlexLayoutProps { interface FlexLayoutProps {
data: RouterOutputs["app"]["byIds"]; data: RouterOutputs["app"]["byIds"];
direction: "row" | "column"; direction: "row" | "column";
hideTitle: boolean;
hideIcon: boolean; hideIcon: boolean;
hideHostname: boolean; hideHostname: boolean;
openNewTab: boolean; openNewTab: boolean;
hasIconColor: boolean; hasIconColor: boolean;
} }
const FlexLayout = ({ data, direction, hideIcon, hideHostname, openNewTab, hasIconColor }: FlexLayoutProps) => { const FlexLayout = ({
data,
direction,
hideTitle,
hideIcon,
hideHostname,
openNewTab,
hasIconColor,
}: FlexLayoutProps) => {
const board = useRequiredBoard(); const board = useRequiredBoard();
return ( return (
<Flex direction={direction} gap="0" w="100%"> <Flex direction={direction} gap="0" w="100%">
{data.map((app, index) => ( {data.map((app) => (
<div key={app.id} style={{ display: "flex", flex: "1", flexDirection: direction }}> <div key={app.id} style={{ display: "flex", flex: "1", flexDirection: direction }}>
<Divider
m="3px"
orientation={direction !== "column" ? "vertical" : "horizontal"}
color={index === 0 ? "transparent" : undefined}
/>
<UnstyledButton <UnstyledButton
component="a" component="a"
href={app.href ?? undefined} href={app.href ?? undefined}
@@ -101,11 +106,23 @@ const FlexLayout = ({ data, direction, hideIcon, hideHostname, openNewTab, hasIc
key={app.id} key={app.id}
w="100%" w="100%"
> >
<Card radius={board.itemRadius} className={classes.card} w="100%" display="flex" p={"xs"} h={"100%"}> <Card radius={board.itemRadius} className={classes.card} w="100%" display="flex" p={4} h="100%">
{direction === "row" ? ( {direction === "row" ? (
<VerticalItem app={app} hideIcon={hideIcon} hideHostname={hideHostname} hasIconColor={hasIconColor} /> <VerticalItem
app={app}
hideTitle={hideTitle}
hideIcon={hideIcon}
hideHostname={hideHostname}
hasIconColor={hasIconColor}
/>
) : ( ) : (
<HorizontalItem app={app} hideIcon={hideIcon} hideHostname={hideHostname} hasIconColor={hasIconColor} /> <HorizontalItem
app={app}
hideTitle={hideTitle}
hideIcon={hideIcon}
hideHostname={hideHostname}
hasIconColor={hasIconColor}
/>
)} )}
</Card> </Card>
</UnstyledButton> </UnstyledButton>
@@ -117,29 +134,27 @@ const FlexLayout = ({ data, direction, hideIcon, hideHostname, openNewTab, hasIc
interface GridLayoutProps { interface GridLayoutProps {
data: RouterOutputs["app"]["byIds"]; data: RouterOutputs["app"]["byIds"];
width: number; hideTitle: boolean;
height: number;
hideIcon: boolean; hideIcon: boolean;
hideHostname: boolean; hideHostname: boolean;
openNewTab: boolean; openNewTab: boolean;
itemDirection: "horizontal" | "vertical";
hasIconColor: boolean; hasIconColor: boolean;
} }
const GridLayout = ({ data, width, height, hideIcon, hideHostname, openNewTab, hasIconColor }: GridLayoutProps) => { const GridLayout = ({
// Calculates the perfect number of columns for the grid layout based on the width and height in pixels and the number of items data,
const columns = Math.ceil(Math.sqrt(data.length * (width / height))); hideTitle,
hideIcon,
hideHostname,
openNewTab,
itemDirection,
hasIconColor,
}: GridLayoutProps) => {
const board = useRequiredBoard(); const board = useRequiredBoard();
return ( return (
<Box <Flex mih="100%" miw="100%" gap={4} wrap="wrap">
display="grid"
h="100%"
style={{
gridTemplateColumns: `repeat(${columns}, 1fr)`,
gap: 10,
}}
>
{data.map((app) => ( {data.map((app) => (
<UnstyledButton <UnstyledButton
component="a" component="a"
@@ -147,46 +162,58 @@ const GridLayout = ({ data, width, height, hideIcon, hideHostname, openNewTab, h
target={openNewTab ? "_blank" : "_self"} target={openNewTab ? "_blank" : "_self"}
rel="noopener noreferrer" rel="noopener noreferrer"
key={app.id} key={app.id}
h="100%" flex="1"
> >
<Card <Card
h="100%" h="100%"
className={combineClasses(classes.card, classes["card-grid"])} className={combineClasses(classes.card, classes["card-grid"])}
radius={board.itemRadius} radius={board.itemRadius}
p="sm" p="xs"
> >
<VerticalItem {itemDirection === "horizontal" ? (
app={app} <HorizontalItem
hideIcon={hideIcon} app={app}
hideHostname={hideHostname} hideTitle={hideTitle}
hasIconColor={hasIconColor} hideIcon={hideIcon}
size={50} hideHostname={hideHostname}
/> hasIconColor={hasIconColor}
/>
) : (
<VerticalItem
app={app}
hideTitle={hideTitle}
hideIcon={hideIcon}
hideHostname={hideHostname}
hasIconColor={hasIconColor}
/>
)}
</Card> </Card>
</UnstyledButton> </UnstyledButton>
))} ))}
</Box> </Flex>
); );
}; };
const VerticalItem = ({ const VerticalItem = ({
app, app,
hideTitle,
hideIcon, hideIcon,
hideHostname, hideHostname,
hasIconColor, hasIconColor,
size = 30,
}: { }: {
app: RouterOutputs["app"]["byIds"][number]; app: RouterOutputs["app"]["byIds"][number];
hideTitle: boolean;
hideIcon: boolean; hideIcon: boolean;
hideHostname: boolean; hideHostname: boolean;
hasIconColor: boolean; hasIconColor: boolean;
size?: number;
}) => { }) => {
return ( return (
<Stack h="100%" gap="sm"> <Stack h="100%" gap="sm">
<Text fw={700} ta="center" size="lg"> {!hideTitle && (
{app.name} <Text fw={700} ta="center" size="xs">
</Text> {app.name}
</Text>
)}
{!hideIcon && ( {!hideIcon && (
<MaskedOrNormalImage <MaskedOrNormalImage
imageUrl={app.iconUrl} imageUrl={app.iconUrl}
@@ -194,18 +221,17 @@ const VerticalItem = ({
alt={app.name} alt={app.name}
className={classes.bookmarkIcon} className={classes.bookmarkIcon}
style={{ style={{
width: size, width: hideHostname && hideTitle ? 16 : 24,
height: size, height: hideHostname && hideTitle ? 16 : 24,
overflow: "auto", overflow: "auto",
flex: 1, flex: 1,
scale: 0.8,
marginLeft: "auto", marginLeft: "auto",
marginRight: "auto", marginRight: "auto",
}} }}
/> />
)} )}
{!hideHostname && ( {!hideHostname && (
<Anchor ta="center" component="span" size="lg"> <Anchor ta="center" component="span" size="xs">
{app.href ? new URL(app.href).hostname : undefined} {app.href ? new URL(app.href).hostname : undefined}
</Anchor> </Anchor>
)} )}
@@ -215,17 +241,19 @@ const VerticalItem = ({
const HorizontalItem = ({ const HorizontalItem = ({
app, app,
hideTitle,
hideIcon, hideIcon,
hideHostname, hideHostname,
hasIconColor, hasIconColor,
}: { }: {
app: RouterOutputs["app"]["byIds"][number]; app: RouterOutputs["app"]["byIds"][number];
hideTitle: boolean;
hideIcon: boolean; hideIcon: boolean;
hideHostname: boolean; hideHostname: boolean;
hasIconColor: boolean; hasIconColor: boolean;
}) => { }) => {
return ( return (
<Group wrap="nowrap" gap={"xs"}> <Group wrap="nowrap" gap="xs" h="100%" justify="center">
{!hideIcon && ( {!hideIcon && (
<MaskedOrNormalImage <MaskedOrNormalImage
imageUrl={app.iconUrl} imageUrl={app.iconUrl}
@@ -234,24 +262,29 @@ const HorizontalItem = ({
className={classes.bookmarkIcon} className={classes.bookmarkIcon}
style={{ style={{
overflow: "auto", overflow: "auto",
scale: 0.8, width: hideHostname ? 16 : 24,
width: 30, height: hideHostname ? 16 : 24,
height: 30,
flex: "unset", flex: "unset",
}} }}
/> />
)} )}
<Stack justify="space-between" gap={0}> {!(hideTitle && hideHostname) && (
<Text fw={700} size="md" lineClamp={1}> <>
{app.name} <Stack justify="space-between" gap={0}>
</Text> {!hideTitle && (
<Text fw={700} size="xs" lineClamp={hideHostname ? 2 : 1}>
{app.name}
</Text>
)}
{!hideHostname && ( {!hideHostname && (
<Anchor component="span" size="xs"> <Anchor component="span" size="xs">
{app.href ? new URL(app.href).hostname : undefined} {app.href ? new URL(app.href).hostname : undefined}
</Anchor> </Anchor>
)} )}
</Stack> </Stack>
</>
)}
</Group> </Group>
); );
}; };

View File

@@ -14,12 +14,13 @@ export const { definition, componentLoader } = createWidgetDefinition("bookmarks
return optionsBuilder.from((factory) => ({ return optionsBuilder.from((factory) => ({
title: factory.text(), title: factory.text(),
layout: factory.select({ layout: factory.select({
options: (["grid", "row", "column"] as const).map((value) => ({ options: (["grid", "gridHorizontal", "row", "column"] as const).map((value) => ({
value, value,
label: (t) => t(`widget.bookmarks.option.layout.option.${value}.label`), label: (t) => t(`widget.bookmarks.option.layout.option.${value}.label`),
})), })),
defaultValue: "column", defaultValue: "column",
}), }),
hideTitle: factory.switch({ defaultValue: false }),
hideIcon: factory.switch({ defaultValue: false }), hideIcon: factory.switch({ defaultValue: false }),
hideHostname: factory.switch({ defaultValue: false }), hideHostname: factory.switch({ defaultValue: false }),
openNewTab: factory.switch({ defaultValue: true }), openNewTab: factory.switch({ defaultValue: true }),