diff --git a/apps/docs/docs/widgets/bookmarks/index.mdx b/apps/docs/docs/widgets/bookmarks/index.mdx
index db2dc85a5..a8808eb6f 100644
--- a/apps/docs/docs/widgets/bookmarks/index.mdx
+++ b/apps/docs/docs/widgets/bookmarks/index.mdx
@@ -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
diff --git a/apps/docs/docs/widgets/bookmarks/index.ts b/apps/docs/docs/widgets/bookmarks/index.ts
index b335a70fd..bfc971457 100644
--- a/apps/docs/docs/widgets/bookmarks/index.ts
+++ b/apps/docs/docs/widgets/bookmarks/index.ts
@@ -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",
},
diff --git a/packages/widgets/src/bookmarks/bookmarks-widget.tsx b/packages/widgets/src/bookmarks/bookmarks-widget.tsx
index 5838af748..968b47303 100644
--- a/packages/widgets/src/bookmarks/bookmarks-widget.tsx
+++ b/packages/widgets/src/bookmarks/bookmarks-widget.tsx
@@ -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 ;
+ const isTight = responsiveHeight < 120;
+
return (
-
+
{options.title.length > 0 ? (
{options.title}
@@ -151,14 +165,18 @@ export default function BookmarksWidget({
) : (
-
-
+
+
{plan.horizontalScroll ? (
-
+
{cards}
) : options.grow ? (
-
+
{cards.map((card) => (
{card}
@@ -166,7 +184,7 @@ export default function BookmarksWidget({
))}
) : (
-
+
{cards}
)}
@@ -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 ? (
-
- ) : null}
+ {showIcon ? : null}
{!iconOnly ? (
{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;
diff --git a/packages/widgets/src/bookmarks/layout.ts b/packages/widgets/src/bookmarks/layout.ts
index 0c0d7b32e..ba84d0001 100644
--- a/packages/widgets/src/bookmarks/layout.ts
+++ b/packages/widgets/src/bookmarks/layout.ts
@@ -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,27 +228,40 @@ 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,
+ showHostname: widthSettings.showHostname && densitySettings.showHostname,
showTitle: widthSettings.showTitle && heightSettings.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,68 @@ 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;
+ }
+
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);
+};