Compare commits
3
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
|
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.
|
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
|
### Screenshots
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ export const bookmarksWidget: WidgetDefinition = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Layout",
|
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"] },
|
values: { type: "select", options: ["Adaptive", "Vertical", "Horizontal", "Grid", "Compact grid", "Icons"] },
|
||||||
defaultValue: "Adaptive",
|
defaultValue: "Adaptive",
|
||||||
},
|
},
|
||||||
@@ -28,7 +29,7 @@ export const bookmarksWidget: WidgetDefinition = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Card spacing",
|
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"] },
|
values: { type: "select", options: ["Extra small", "Small", "Medium", "Large", "Extra large"] },
|
||||||
defaultValue: "Extra small",
|
defaultValue: "Extra small",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export default function BookmarksWidget({
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
displayMode,
|
displayMode,
|
||||||
|
displayScale = 1,
|
||||||
}: WidgetComponentProps<"bookmarks">) {
|
}: WidgetComponentProps<"bookmarks">) {
|
||||||
const t = useI18n("widget.bookmarks");
|
const t = useI18n("widget.bookmarks");
|
||||||
const board = useRequiredBoard();
|
const board = useRequiredBoard();
|
||||||
@@ -66,6 +67,16 @@ export default function BookmarksWidget({
|
|||||||
});
|
});
|
||||||
const data = [...configuredItems, ...legacyItems];
|
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(
|
useRegisterSpotlightContextResults(
|
||||||
`bookmark-${itemId}`,
|
`bookmark-${itemId}`,
|
||||||
data.flatMap((bookmark) => {
|
data.flatMap((bookmark) => {
|
||||||
@@ -95,12 +106,13 @@ export default function BookmarksWidget({
|
|||||||
() =>
|
() =>
|
||||||
getBookmarkDisplayPlan({
|
getBookmarkDisplayPlan({
|
||||||
advanced,
|
advanced,
|
||||||
height: options.title.length > 0 ? height - 36 : height,
|
gap: bookmarkSpacingPixels[options.spacing],
|
||||||
|
height: contentHeight,
|
||||||
itemCount: data.length,
|
itemCount: data.length,
|
||||||
layout: options.layout,
|
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({
|
const cardDisplay = getBookmarkCardDisplay({
|
||||||
@@ -131,8 +143,10 @@ export default function BookmarksWidget({
|
|||||||
|
|
||||||
if (appIds.length > 0 && isInitialWidgetQueryPending(appsQuery)) return <WidgetQueryLoadingState />;
|
if (appIds.length > 0 && isInitialWidgetQueryPending(appsQuery)) return <WidgetQueryLoadingState />;
|
||||||
|
|
||||||
|
const isTight = responsiveHeight < 120;
|
||||||
|
|
||||||
return (
|
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 ? (
|
{options.title.length > 0 ? (
|
||||||
<Text fz={11} fw={600} px={2} lh={1.2} lineClamp={1}>
|
<Text fz={11} fw={600} px={2} lh={1.2} lineClamp={1}>
|
||||||
{options.title}
|
{options.title}
|
||||||
@@ -151,14 +165,18 @@ export default function BookmarksWidget({
|
|||||||
</Stack>
|
</Stack>
|
||||||
</Center>
|
</Center>
|
||||||
) : (
|
) : (
|
||||||
<ScrollArea scrollbars={plan.horizontalScroll ? "x" : "y"} style={{ flex: 1, minHeight: 0 }}>
|
<ScrollArea
|
||||||
<Box miw="100%" mih="100%" pb={2}>
|
scrollbars={plan.horizontalScroll ? "x" : "y"}
|
||||||
|
style={{ flex: 1, minHeight: 0 }}
|
||||||
|
styles={{ content: { height: "100%" } }}
|
||||||
|
>
|
||||||
|
<Box miw="100%" h="100%" pb={2}>
|
||||||
{plan.horizontalScroll ? (
|
{plan.horizontalScroll ? (
|
||||||
<Group gap={options.spacing} wrap="nowrap" mih="100%" align="stretch">
|
<Group gap={plan.itemGap} wrap="nowrap" h="100%" align="center">
|
||||||
{cards}
|
{cards}
|
||||||
</Group>
|
</Group>
|
||||||
) : options.grow ? (
|
) : options.grow ? (
|
||||||
<Grid grow columns={plan.columns} gap={options.spacing}>
|
<Grid grow columns={plan.columns} gap={plan.itemGap}>
|
||||||
{cards.map((card) => (
|
{cards.map((card) => (
|
||||||
<Grid.Col key={card.key} span={1}>
|
<Grid.Col key={card.key} span={1}>
|
||||||
{card}
|
{card}
|
||||||
@@ -166,7 +184,7 @@ export default function BookmarksWidget({
|
|||||||
))}
|
))}
|
||||||
</Grid>
|
</Grid>
|
||||||
) : (
|
) : (
|
||||||
<SimpleGrid cols={plan.columns} spacing={options.spacing} verticalSpacing={options.spacing}>
|
<SimpleGrid cols={plan.columns} spacing={plan.itemGap} verticalSpacing={plan.itemGap}>
|
||||||
{cards}
|
{cards}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
)}
|
)}
|
||||||
@@ -213,6 +231,17 @@ const BookmarkCard = ({
|
|||||||
const iconUrl = bookmark.iconUrl ?? getBookmarkFaviconUrl(bookmark.href);
|
const iconUrl = bookmark.iconUrl ?? getBookmarkFaviconUrl(bookmark.href);
|
||||||
const iconOnly = orientation === "icon" || (!showTitle && !showHostname);
|
const iconOnly = orientation === "icon" || (!showTitle && !showHostname);
|
||||||
const background = getBookmarkBackground(variant, active);
|
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 =
|
const content =
|
||||||
orientation === "vertical" && !advanced ? (
|
orientation === "vertical" && !advanced ? (
|
||||||
@@ -238,9 +267,7 @@ const BookmarkCard = ({
|
|||||||
align="center"
|
align="center"
|
||||||
flex={1}
|
flex={1}
|
||||||
>
|
>
|
||||||
{showIcon ? (
|
{showIcon ? <BookmarkAvatar bookmark={bookmark} iconUrl={iconUrl} size={avatarSize} /> : null}
|
||||||
<BookmarkAvatar bookmark={bookmark} iconUrl={iconUrl} size={advanced ? 42 : iconOnly ? 32 : 30} />
|
|
||||||
) : null}
|
|
||||||
{!iconOnly ? (
|
{!iconOnly ? (
|
||||||
<Stack gap={advanced ? 3 : 0} miw={0} flex={1}>
|
<Stack gap={advanced ? 3 : 0} miw={0} flex={1}>
|
||||||
{showTitle ? (
|
{showTitle ? (
|
||||||
@@ -304,7 +331,7 @@ const BookmarkCard = ({
|
|||||||
aria-label={bookmark.name}
|
aria-label={bookmark.name}
|
||||||
radius={radius}
|
radius={radius}
|
||||||
withBorder={withBorder || variant === "outline"}
|
withBorder={withBorder || variant === "outline"}
|
||||||
p={iconOnly ? 6 : advanced ? "md" : "xs"}
|
p={padding}
|
||||||
h={height}
|
h={height}
|
||||||
w={width}
|
w={width}
|
||||||
miw={width}
|
miw={width}
|
||||||
@@ -371,3 +398,11 @@ const getBookmarkHostname = (href: string): string | undefined => {
|
|||||||
return 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 {
|
export interface BookmarkDisplayPlan {
|
||||||
columns: number;
|
columns: number;
|
||||||
horizontalScroll: boolean;
|
horizontalScroll: boolean;
|
||||||
|
itemGap: number;
|
||||||
itemHeight: number;
|
itemHeight: number;
|
||||||
itemWidth: number;
|
itemWidth: number;
|
||||||
orientation: BookmarkOrientation;
|
orientation: BookmarkOrientation;
|
||||||
@@ -107,6 +108,18 @@ const widthBreakpoints = [
|
|||||||
showHostname: false,
|
showHostname: false,
|
||||||
showTitle: true,
|
showTitle: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
minWidth: 180,
|
||||||
|
adaptiveColumns: 1,
|
||||||
|
advancedColumns: 1,
|
||||||
|
compactColumns: 1,
|
||||||
|
gridColumns: 1,
|
||||||
|
iconColumns: 3,
|
||||||
|
rowItemWidth: 136,
|
||||||
|
adaptiveOrientation: "horizontal",
|
||||||
|
showHostname: false,
|
||||||
|
showTitle: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
adaptiveColumns: 1,
|
adaptiveColumns: 1,
|
||||||
@@ -160,24 +173,35 @@ const heightBreakpoints = [
|
|||||||
showTitle: true,
|
showTitle: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
minHeight: 0,
|
minHeight: 60,
|
||||||
columnItemHeight: 48,
|
columnItemHeight: 48,
|
||||||
itemHeight: 56,
|
itemHeight: 48,
|
||||||
rowItemHeight: 56,
|
rowItemHeight: 56,
|
||||||
orientation: "icon",
|
orientation: "icon",
|
||||||
showHostname: false,
|
showHostname: false,
|
||||||
showTitle: false,
|
showTitle: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
minHeight: 0,
|
||||||
|
columnItemHeight: 32,
|
||||||
|
itemHeight: 32,
|
||||||
|
rowItemHeight: 40,
|
||||||
|
orientation: "icon",
|
||||||
|
showHostname: false,
|
||||||
|
showTitle: false,
|
||||||
|
},
|
||||||
] as const satisfies readonly BookmarkHeightBreakpoint[];
|
] as const satisfies readonly BookmarkHeightBreakpoint[];
|
||||||
|
|
||||||
export const getBookmarkDisplayPlan = ({
|
export const getBookmarkDisplayPlan = ({
|
||||||
advanced,
|
advanced,
|
||||||
|
gap = 0,
|
||||||
height,
|
height,
|
||||||
itemCount,
|
itemCount,
|
||||||
layout,
|
layout,
|
||||||
width,
|
width,
|
||||||
}: {
|
}: {
|
||||||
advanced: boolean;
|
advanced: boolean;
|
||||||
|
gap?: number;
|
||||||
height: number;
|
height: number;
|
||||||
itemCount: number;
|
itemCount: number;
|
||||||
layout: BookmarkLayout;
|
layout: BookmarkLayout;
|
||||||
@@ -194,6 +218,7 @@ export const getBookmarkDisplayPlan = ({
|
|||||||
return {
|
return {
|
||||||
columns: Math.min(count, widthSettings.advancedColumns),
|
columns: Math.min(count, widthSettings.advancedColumns),
|
||||||
horizontalScroll: false,
|
horizontalScroll: false,
|
||||||
|
itemGap: gap,
|
||||||
itemHeight: 104,
|
itemHeight: 104,
|
||||||
itemWidth: 260,
|
itemWidth: 260,
|
||||||
orientation: "horizontal",
|
orientation: "horizontal",
|
||||||
@@ -203,28 +228,41 @@ export const getBookmarkDisplayPlan = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (layout === "row") {
|
if (layout === "row") {
|
||||||
let orientation: BookmarkOrientation = "vertical";
|
let orientation: BookmarkOrientation = "horizontal";
|
||||||
if (!heightSettings.showTitle) orientation = "icon";
|
if (!widthSettings.showTitle) orientation = "icon";
|
||||||
|
let itemWidth: number = widthSettings.rowItemWidth;
|
||||||
|
if (heightSettings.rowItemHeight <= 56) itemWidth = 112;
|
||||||
|
if (heightSettings.rowItemHeight <= 40) itemWidth = 96;
|
||||||
return {
|
return {
|
||||||
columns: count,
|
columns: count,
|
||||||
horizontalScroll: true,
|
horizontalScroll: true,
|
||||||
itemHeight: heightSettings.rowItemHeight,
|
itemGap: Math.min(gap, 8),
|
||||||
itemWidth: widthSettings.rowItemWidth,
|
itemHeight: Math.min(heightSettings.rowItemHeight, 64),
|
||||||
|
itemWidth,
|
||||||
orientation,
|
orientation,
|
||||||
showHostname: widthSettings.showHostname && heightSettings.showHostname,
|
showHostname: widthSettings.showHostname && heightSettings.showHostname,
|
||||||
showTitle: widthSettings.showTitle && heightSettings.showTitle,
|
showTitle: widthSettings.showTitle,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (layout === "column") {
|
if (layout === "column") {
|
||||||
|
const densitySettings = getBookmarkDensitySettings({
|
||||||
|
columns: 1,
|
||||||
|
gap,
|
||||||
|
height: normalizedHeight,
|
||||||
|
heightProperty: "columnItemHeight",
|
||||||
|
itemCount: count,
|
||||||
|
preferredSettings: heightSettings,
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
columns: 1,
|
columns: 1,
|
||||||
horizontalScroll: false,
|
horizontalScroll: false,
|
||||||
itemHeight: heightSettings.columnItemHeight,
|
itemGap: getBookmarkDensityGap(gap, densitySettings),
|
||||||
|
itemHeight: densitySettings.columnItemHeight,
|
||||||
itemWidth: width,
|
itemWidth: width,
|
||||||
orientation: "horizontal",
|
orientation: "horizontal",
|
||||||
showHostname: widthSettings.showHostname && heightSettings.showHostname,
|
showHostname: widthSettings.showHostname && densitySettings.showHostname,
|
||||||
showTitle: widthSettings.showTitle && heightSettings.showTitle,
|
showTitle: widthSettings.showTitle && densitySettings.showTitle,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,6 +270,7 @@ export const getBookmarkDisplayPlan = ({
|
|||||||
return {
|
return {
|
||||||
columns: Math.min(count, widthSettings.iconColumns),
|
columns: Math.min(count, widthSettings.iconColumns),
|
||||||
horizontalScroll: false,
|
horizontalScroll: false,
|
||||||
|
itemGap: Math.min(gap, 8),
|
||||||
itemHeight: 56,
|
itemHeight: 56,
|
||||||
itemWidth: 56,
|
itemWidth: 56,
|
||||||
orientation: "icon",
|
orientation: "icon",
|
||||||
@@ -244,6 +283,7 @@ export const getBookmarkDisplayPlan = ({
|
|||||||
return {
|
return {
|
||||||
columns: Math.min(count, widthSettings.compactColumns),
|
columns: Math.min(count, widthSettings.compactColumns),
|
||||||
horizontalScroll: false,
|
horizontalScroll: false,
|
||||||
|
itemGap: Math.min(gap, 8),
|
||||||
itemHeight: 56,
|
itemHeight: 56,
|
||||||
itemWidth: widthSettings.rowItemWidth,
|
itemWidth: widthSettings.rowItemWidth,
|
||||||
orientation: "horizontal",
|
orientation: "horizontal",
|
||||||
@@ -265,13 +305,69 @@ export const getBookmarkDisplayPlan = ({
|
|||||||
if (heightSettings.orientation === "horizontal" && orientation === "vertical") orientation = "horizontal";
|
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 {
|
return {
|
||||||
columns: Math.min(count, columns),
|
columns: visibleColumns,
|
||||||
horizontalScroll: false,
|
horizontalScroll: false,
|
||||||
itemHeight: heightSettings.itemHeight,
|
itemGap: getBookmarkDensityGap(gap, densitySettings),
|
||||||
|
itemHeight: densitySettings.itemHeight,
|
||||||
itemWidth: widthSettings.rowItemWidth,
|
itemWidth: widthSettings.rowItemWidth,
|
||||||
orientation,
|
orientation,
|
||||||
showHostname: orientation !== "icon" && widthSettings.showHostname && heightSettings.showHostname,
|
showHostname,
|
||||||
showTitle: orientation !== "icon" && widthSettings.showTitle && heightSettings.showTitle,
|
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