Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
620c13db9c | ||
|
|
58f4436485 | ||
|
|
81daa87b9e |
No files matched your search
@@ -15,6 +15,10 @@ import { bookmarksWidget } from ".";
|
||||
|
||||
The Bookmarks widget keeps direct URLs and Homarr Apps in one ordered list. URLs do not require an App record; Homarr
|
||||
uses the site's favicon when available. Choose adaptive, compact-grid, or other layouts from the widget configuration.
|
||||
Adaptive, vertical, and grid layouts keep larger cards when the configured bookmarks fit, then reduce card height and
|
||||
spacing to show as many bookmarks as possible before scrolling. Horizontal uses a compact, centered scrolling row with
|
||||
narrower cards in one-row widgets. Horizontal, compact-grid, and icon-only layouts keep their gaps tight. Compact grid
|
||||
also preserves its configured columns when the dashboard is scaled.
|
||||
|
||||
### Screenshots
|
||||
|
||||
|
||||
@@ -16,7 +16,8 @@ export const bookmarksWidget: WidgetDefinition = {
|
||||
},
|
||||
{
|
||||
name: "Layout",
|
||||
description: "How bookmarks use the available widget space.",
|
||||
description:
|
||||
"How bookmarks use the available widget space. Layouts stay compact and reduce card size when needed.",
|
||||
values: { type: "select", options: ["Adaptive", "Vertical", "Horizontal", "Grid", "Compact grid", "Icons"] },
|
||||
defaultValue: "Adaptive",
|
||||
},
|
||||
@@ -28,7 +29,7 @@ export const bookmarksWidget: WidgetDefinition = {
|
||||
},
|
||||
{
|
||||
name: "Card spacing",
|
||||
description: "Space between bookmark cards.",
|
||||
description: "Space between bookmark cards. Horizontal, compact-grid, and icon-only layouts cap larger gaps.",
|
||||
values: { type: "select", options: ["Extra small", "Small", "Medium", "Large", "Extra large"] },
|
||||
defaultValue: "Extra small",
|
||||
},
|
||||
|
||||
@@ -41,6 +41,7 @@ export default function BookmarksWidget({
|
||||
width,
|
||||
height,
|
||||
displayMode,
|
||||
displayScale = 1,
|
||||
}: WidgetComponentProps<"bookmarks">) {
|
||||
const t = useI18n("widget.bookmarks");
|
||||
const board = useRequiredBoard();
|
||||
@@ -66,6 +67,16 @@ export default function BookmarksWidget({
|
||||
});
|
||||
const data = [...configuredItems, ...legacyItems];
|
||||
|
||||
let responsiveWidth = width;
|
||||
let responsiveHeight = height;
|
||||
if (!advanced && Number.isFinite(displayScale) && displayScale > 0) {
|
||||
responsiveWidth *= displayScale;
|
||||
responsiveHeight *= displayScale;
|
||||
}
|
||||
const contentHeight = Math.max(0, responsiveHeight - (options.title.length > 0 ? 36 : 0));
|
||||
let layoutWidth = responsiveWidth;
|
||||
if (options.layout === "gridHorizontal") layoutWidth = width;
|
||||
|
||||
useRegisterSpotlightContextResults(
|
||||
`bookmark-${itemId}`,
|
||||
data.flatMap((bookmark) => {
|
||||
@@ -95,12 +106,13 @@ export default function BookmarksWidget({
|
||||
() =>
|
||||
getBookmarkDisplayPlan({
|
||||
advanced,
|
||||
height: options.title.length > 0 ? height - 36 : height,
|
||||
gap: bookmarkSpacingPixels[options.spacing],
|
||||
height: contentHeight,
|
||||
itemCount: data.length,
|
||||
layout: options.layout,
|
||||
width,
|
||||
width: layoutWidth,
|
||||
}),
|
||||
[advanced, data.length, height, options.layout, options.title.length, width],
|
||||
[advanced, contentHeight, data.length, layoutWidth, options.layout, options.spacing],
|
||||
);
|
||||
|
||||
const cardDisplay = getBookmarkCardDisplay({
|
||||
@@ -131,8 +143,10 @@ export default function BookmarksWidget({
|
||||
|
||||
if (appIds.length > 0 && isInitialWidgetQueryPending(appsQuery)) return <WidgetQueryLoadingState />;
|
||||
|
||||
const isTight = responsiveHeight < 120;
|
||||
|
||||
return (
|
||||
<Stack h="100%" mih={0} gap={height < 120 ? 6 : "sm"} p={height < 120 ? 6 : "sm"}>
|
||||
<Stack h="100%" mih={0} gap={isTight ? 6 : "sm"} p={isTight ? 6 : "sm"}>
|
||||
{options.title.length > 0 ? (
|
||||
<Text fz={11} fw={600} px={2} lh={1.2} lineClamp={1}>
|
||||
{options.title}
|
||||
@@ -151,14 +165,18 @@ export default function BookmarksWidget({
|
||||
</Stack>
|
||||
</Center>
|
||||
) : (
|
||||
<ScrollArea scrollbars={plan.horizontalScroll ? "x" : "y"} style={{ flex: 1, minHeight: 0 }}>
|
||||
<Box miw="100%" mih="100%" pb={2}>
|
||||
<ScrollArea
|
||||
scrollbars={plan.horizontalScroll ? "x" : "y"}
|
||||
style={{ flex: 1, minHeight: 0 }}
|
||||
styles={{ content: { height: "100%" } }}
|
||||
>
|
||||
<Box miw="100%" h="100%" pb={2}>
|
||||
{plan.horizontalScroll ? (
|
||||
<Group gap={options.spacing} wrap="nowrap" mih="100%" align="stretch">
|
||||
<Group gap={plan.itemGap} wrap="nowrap" h="100%" align="center">
|
||||
{cards}
|
||||
</Group>
|
||||
) : options.grow ? (
|
||||
<Grid grow columns={plan.columns} gap={options.spacing}>
|
||||
<Grid grow columns={plan.columns} gap={plan.itemGap}>
|
||||
{cards.map((card) => (
|
||||
<Grid.Col key={card.key} span={1}>
|
||||
{card}
|
||||
@@ -166,7 +184,7 @@ export default function BookmarksWidget({
|
||||
))}
|
||||
</Grid>
|
||||
) : (
|
||||
<SimpleGrid cols={plan.columns} spacing={options.spacing} verticalSpacing={options.spacing}>
|
||||
<SimpleGrid cols={plan.columns} spacing={plan.itemGap} verticalSpacing={plan.itemGap}>
|
||||
{cards}
|
||||
</SimpleGrid>
|
||||
)}
|
||||
@@ -213,6 +231,17 @@ const BookmarkCard = ({
|
||||
const iconUrl = bookmark.iconUrl ?? getBookmarkFaviconUrl(bookmark.href);
|
||||
const iconOnly = orientation === "icon" || (!showTitle && !showHostname);
|
||||
const background = getBookmarkBackground(variant, active);
|
||||
const isUltraDense = height <= 32;
|
||||
const isDense = height <= 48;
|
||||
let avatarSize = 30;
|
||||
if (isDense) avatarSize = 28;
|
||||
if (isUltraDense) avatarSize = 20;
|
||||
if (iconOnly && !isDense) avatarSize = 32;
|
||||
if (advanced) avatarSize = 42;
|
||||
let padding: number | "xs" | "md" = "xs";
|
||||
if (isDense || iconOnly) padding = 6;
|
||||
if (isUltraDense) padding = 4;
|
||||
if (advanced) padding = "md";
|
||||
|
||||
const content =
|
||||
orientation === "vertical" && !advanced ? (
|
||||
@@ -238,9 +267,7 @@ const BookmarkCard = ({
|
||||
align="center"
|
||||
flex={1}
|
||||
>
|
||||
{showIcon ? (
|
||||
<BookmarkAvatar bookmark={bookmark} iconUrl={iconUrl} size={advanced ? 42 : iconOnly ? 32 : 30} />
|
||||
) : null}
|
||||
{showIcon ? <BookmarkAvatar bookmark={bookmark} iconUrl={iconUrl} size={avatarSize} /> : null}
|
||||
{!iconOnly ? (
|
||||
<Stack gap={advanced ? 3 : 0} miw={0} flex={1}>
|
||||
{showTitle ? (
|
||||
@@ -304,7 +331,7 @@ const BookmarkCard = ({
|
||||
aria-label={bookmark.name}
|
||||
radius={radius}
|
||||
withBorder={withBorder || variant === "outline"}
|
||||
p={iconOnly ? 6 : advanced ? "md" : "xs"}
|
||||
p={padding}
|
||||
h={height}
|
||||
w={width}
|
||||
miw={width}
|
||||
@@ -371,3 +398,11 @@ const getBookmarkHostname = (href: string): string | undefined => {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const bookmarkSpacingPixels = {
|
||||
xs: 10,
|
||||
sm: 12,
|
||||
md: 16,
|
||||
lg: 20,
|
||||
xl: 32,
|
||||
} as const;
|
||||
@@ -6,6 +6,7 @@ export type BookmarkOrientation = "horizontal" | "vertical" | "icon";
|
||||
export interface BookmarkDisplayPlan {
|
||||
columns: number;
|
||||
horizontalScroll: boolean;
|
||||
itemGap: number;
|
||||
itemHeight: number;
|
||||
itemWidth: number;
|
||||
orientation: BookmarkOrientation;
|
||||
@@ -107,6 +108,18 @@ const widthBreakpoints = [
|
||||
showHostname: false,
|
||||
showTitle: true,
|
||||
},
|
||||
{
|
||||
minWidth: 180,
|
||||
adaptiveColumns: 1,
|
||||
advancedColumns: 1,
|
||||
compactColumns: 1,
|
||||
gridColumns: 1,
|
||||
iconColumns: 3,
|
||||
rowItemWidth: 136,
|
||||
adaptiveOrientation: "horizontal",
|
||||
showHostname: false,
|
||||
showTitle: true,
|
||||
},
|
||||
{
|
||||
minWidth: 0,
|
||||
adaptiveColumns: 1,
|
||||
@@ -160,24 +173,35 @@ const heightBreakpoints = [
|
||||
showTitle: true,
|
||||
},
|
||||
{
|
||||
minHeight: 0,
|
||||
minHeight: 60,
|
||||
columnItemHeight: 48,
|
||||
itemHeight: 56,
|
||||
itemHeight: 48,
|
||||
rowItemHeight: 56,
|
||||
orientation: "icon",
|
||||
showHostname: false,
|
||||
showTitle: false,
|
||||
},
|
||||
{
|
||||
minHeight: 0,
|
||||
columnItemHeight: 32,
|
||||
itemHeight: 32,
|
||||
rowItemHeight: 40,
|
||||
orientation: "icon",
|
||||
showHostname: false,
|
||||
showTitle: false,
|
||||
},
|
||||
] as const satisfies readonly BookmarkHeightBreakpoint[];
|
||||
|
||||
export const getBookmarkDisplayPlan = ({
|
||||
advanced,
|
||||
gap = 0,
|
||||
height,
|
||||
itemCount,
|
||||
layout,
|
||||
width,
|
||||
}: {
|
||||
advanced: boolean;
|
||||
gap?: number;
|
||||
height: number;
|
||||
itemCount: number;
|
||||
layout: BookmarkLayout;
|
||||
@@ -194,6 +218,7 @@ export const getBookmarkDisplayPlan = ({
|
||||
return {
|
||||
columns: Math.min(count, widthSettings.advancedColumns),
|
||||
horizontalScroll: false,
|
||||
itemGap: gap,
|
||||
itemHeight: 104,
|
||||
itemWidth: 260,
|
||||
orientation: "horizontal",
|
||||
@@ -203,28 +228,41 @@ export const getBookmarkDisplayPlan = ({
|
||||
}
|
||||
|
||||
if (layout === "row") {
|
||||
let orientation: BookmarkOrientation = "vertical";
|
||||
if (!heightSettings.showTitle) orientation = "icon";
|
||||
let orientation: BookmarkOrientation = "horizontal";
|
||||
if (!widthSettings.showTitle) orientation = "icon";
|
||||
let itemWidth: number = widthSettings.rowItemWidth;
|
||||
if (heightSettings.rowItemHeight <= 56) itemWidth = 112;
|
||||
if (heightSettings.rowItemHeight <= 40) itemWidth = 96;
|
||||
return {
|
||||
columns: count,
|
||||
horizontalScroll: true,
|
||||
itemHeight: heightSettings.rowItemHeight,
|
||||
itemWidth: widthSettings.rowItemWidth,
|
||||
itemGap: Math.min(gap, 8),
|
||||
itemHeight: Math.min(heightSettings.rowItemHeight, 64),
|
||||
itemWidth,
|
||||
orientation,
|
||||
showHostname: widthSettings.showHostname && heightSettings.showHostname,
|
||||
showTitle: widthSettings.showTitle && heightSettings.showTitle,
|
||||
showTitle: widthSettings.showTitle,
|
||||
};
|
||||
}
|
||||
|
||||
if (layout === "column") {
|
||||
const densitySettings = getBookmarkDensitySettings({
|
||||
columns: 1,
|
||||
gap,
|
||||
height: normalizedHeight,
|
||||
heightProperty: "columnItemHeight",
|
||||
itemCount: count,
|
||||
preferredSettings: heightSettings,
|
||||
});
|
||||
return {
|
||||
columns: 1,
|
||||
horizontalScroll: false,
|
||||
itemHeight: heightSettings.columnItemHeight,
|
||||
itemGap: getBookmarkDensityGap(gap, densitySettings),
|
||||
itemHeight: densitySettings.columnItemHeight,
|
||||
itemWidth: width,
|
||||
orientation: "horizontal",
|
||||
showHostname: widthSettings.showHostname && heightSettings.showHostname,
|
||||
showTitle: widthSettings.showTitle && heightSettings.showTitle,
|
||||
showHostname: widthSettings.showHostname && densitySettings.showHostname,
|
||||
showTitle: widthSettings.showTitle && densitySettings.showTitle,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -232,6 +270,7 @@ export const getBookmarkDisplayPlan = ({
|
||||
return {
|
||||
columns: Math.min(count, widthSettings.iconColumns),
|
||||
horizontalScroll: false,
|
||||
itemGap: Math.min(gap, 8),
|
||||
itemHeight: 56,
|
||||
itemWidth: 56,
|
||||
orientation: "icon",
|
||||
@@ -244,6 +283,7 @@ export const getBookmarkDisplayPlan = ({
|
||||
return {
|
||||
columns: Math.min(count, widthSettings.compactColumns),
|
||||
horizontalScroll: false,
|
||||
itemGap: Math.min(gap, 8),
|
||||
itemHeight: 56,
|
||||
itemWidth: widthSettings.rowItemWidth,
|
||||
orientation: "horizontal",
|
||||
@@ -265,13 +305,69 @@ export const getBookmarkDisplayPlan = ({
|
||||
if (heightSettings.orientation === "horizontal" && orientation === "vertical") orientation = "horizontal";
|
||||
}
|
||||
|
||||
const visibleColumns = Math.min(count, columns);
|
||||
const densitySettings = getBookmarkDensitySettings({
|
||||
columns: visibleColumns,
|
||||
gap,
|
||||
height: normalizedHeight,
|
||||
heightProperty: "itemHeight",
|
||||
itemCount: count,
|
||||
preferredSettings: heightSettings,
|
||||
});
|
||||
if (layout === "adaptive" && densitySettings.itemHeight < 80 && orientation === "vertical") {
|
||||
orientation = "horizontal";
|
||||
}
|
||||
|
||||
let showHostname =
|
||||
orientation !== "icon" && widthSettings.showHostname && heightSettings.showHostname && densitySettings.showHostname;
|
||||
let showTitle = orientation !== "icon" && widthSettings.showTitle && heightSettings.showTitle;
|
||||
if (layout === "grid") {
|
||||
showHostname = showHostname && densitySettings.showHostname;
|
||||
showTitle = showTitle && densitySettings.showTitle;
|
||||
if (!showHostname && !showTitle) orientation = "icon";
|
||||
}
|
||||
|
||||
return {
|
||||
columns: Math.min(count, columns),
|
||||
columns: visibleColumns,
|
||||
horizontalScroll: false,
|
||||
itemHeight: heightSettings.itemHeight,
|
||||
itemGap: getBookmarkDensityGap(gap, densitySettings),
|
||||
itemHeight: densitySettings.itemHeight,
|
||||
itemWidth: widthSettings.rowItemWidth,
|
||||
orientation,
|
||||
showHostname: orientation !== "icon" && widthSettings.showHostname && heightSettings.showHostname,
|
||||
showTitle: orientation !== "icon" && widthSettings.showTitle && heightSettings.showTitle,
|
||||
showHostname,
|
||||
showTitle,
|
||||
};
|
||||
};
|
||||
|
||||
type BookmarkHeightProperty = "columnItemHeight" | "itemHeight";
|
||||
type BookmarkHeightSettings = (typeof heightBreakpoints)[number];
|
||||
|
||||
const getBookmarkDensitySettings = ({
|
||||
columns,
|
||||
gap,
|
||||
height,
|
||||
heightProperty,
|
||||
itemCount,
|
||||
preferredSettings,
|
||||
}: {
|
||||
columns: number;
|
||||
gap: number;
|
||||
height: number;
|
||||
heightProperty: BookmarkHeightProperty;
|
||||
itemCount: number;
|
||||
preferredSettings: BookmarkHeightSettings;
|
||||
}): BookmarkHeightSettings => {
|
||||
const rowCount = Math.ceil(itemCount / Math.max(1, columns));
|
||||
const totalGap = Math.max(0, rowCount - 1) * Math.max(0, gap);
|
||||
const preferredIndex = heightBreakpoints.indexOf(preferredSettings);
|
||||
const matchingSettings = heightBreakpoints
|
||||
.slice(preferredIndex)
|
||||
.find((settings) => rowCount * settings[heightProperty] + totalGap <= height);
|
||||
|
||||
return matchingSettings ?? heightBreakpoints[4];
|
||||
};
|
||||
|
||||
const getBookmarkDensityGap = (gap: number, settings: BookmarkHeightSettings) => {
|
||||
if (settings.itemHeight > 32 && settings.columnItemHeight > 32) return gap;
|
||||
return Math.min(gap, 4);
|
||||
};
|
||||
Reference in new issue
Block a user