Compare commits

..
Author SHA1 Message Date
ajnart e29525a5fc fix(weather): handle disabled external connections 2026-09-06 15:34:45 +02:00
42 changed files with 44 additions and 370 deletions

No files matched your search

@@ -20,7 +20,7 @@ jobs:
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.RENOVATE_APPROVE_PRIVATE_KEY }}
client-id: ${{ vars.RENOVATE_APPROVE_CLIENT_ID }}
app-id: ${{ secrets.RENOVATE_APPROVE_APP_ID }}
permission-pull-requests: write # required to enable auto-merge
- name: Install GitHub CLI
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.RENOVATE_APPROVE_PRIVATE_KEY }}
client-id: ${{ vars.RENOVATE_APPROVE_CLIENT_ID }}
app-id: ${{ secrets.RENOVATE_APPROVE_APP_ID }}
permission-pull-requests: write # required to approve a pull request and enable auto-merge
- name: Install GitHub CLI
@@ -22,7 +22,7 @@ jobs:
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.CROWDIN_APP_PRIVATE_KEY }}
client-id: ${{ vars.CROWDIN_CLIENT_ID }}
app-id: ${{ secrets.CROWDIN_APP_ID }}
permission-contents: write # required to commit to crowdin branch
permission-pull-requests: write # required to create pull request
@@ -52,7 +52,7 @@ jobs:
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.RENOVATE_APPROVE_PRIVATE_KEY }}
client-id: ${{ vars.RENOVATE_APPROVE_CLIENT_ID }}
app-id: ${{ secrets.RENOVATE_APPROVE_APP_ID }}
permission-pull-requests: write
- name: Approve PR
env:
@@ -64,7 +64,7 @@ jobs:
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
client-id: ${{ vars.RENOVATE_MERGE_CLIENT_ID }}
app-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
permission-contents: write # write to beta branch (due to merge)
permission-pull-requests: write # merge pull request
- id: automerge
@@ -55,7 +55,7 @@ jobs:
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
client-id: ${{ vars.RENOVATE_MERGE_CLIENT_ID }}
app-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
permission-contents: write # required to commit package.json & changelog changes, merge them to dev / next and publish the release
- uses: actions/checkout@v7
@@ -277,7 +277,7 @@ jobs:
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
client-id: ${{ vars.RENOVATE_MERGE_CLIENT_ID }}
app-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
permission-contents: write # required to publish the release
- name: Log in to the Container registry
@@ -73,7 +73,7 @@ jobs:
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.RENOVATE_APPROVE_PRIVATE_KEY }}
client-id: ${{ vars.RENOVATE_APPROVE_CLIENT_ID }}
app-id: ${{ secrets.RENOVATE_APPROVE_APP_ID }}
permission-pull-requests: write
- name: Approve PR
if: ${{ steps.create-pull-request.outcome == 'success' }}
@@ -87,7 +87,7 @@ jobs:
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
client-id: ${{ vars.RENOVATE_MERGE_CLIENT_ID }}
app-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
permission-contents: write # write to main branch (due to merge)
permission-pull-requests: write # merge pull request
- id: automerge
+1 -1
View File
@@ -15,7 +15,7 @@ jobs:
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
client-id: ${{ vars.RENOVATE_MERGE_CLIENT_ID }}
app-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
permission-contents: write # required to commit to branch
permission-pull-requests: write # required to create pr & enable automerge
- name: Checkout code
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.HOMARR_UPDATE_CONTRIBUTORS_PRIVATE_KEY }}
client-id: ${{ vars.HOMARR_UPDATE_CONTRIBUTORS_CLIENT_ID }}
app-id: ${{ vars.HOMARR_UPDATE_CONTRIBUTORS_APP_ID }}
permission-contents: write # required to commit to branch
permission-pull-requests: write # required to create pr & enable automerge
@@ -24,7 +24,7 @@ jobs:
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.HOMARR_UPDATE_CONTRIBUTORS_PRIVATE_KEY }}
client-id: ${{ vars.HOMARR_UPDATE_CONTRIBUTORS_CLIENT_ID }}
app-id: ${{ vars.HOMARR_UPDATE_CONTRIBUTORS_APP_ID }}
permission-contents: write # required to commit to branch
permission-pull-requests: write # required to create pr & enable automerge
- name: Checkout code
+1 -5
View File
@@ -10,11 +10,7 @@
"CHANGELOG.md",
"apps/docs/.docusaurus/**",
"apps/docs/build/**",
"apps/docs/blog/2022/**",
"apps/docs/blog/2023/**",
"apps/docs/blog/2024/**",
"apps/docs/blog/2025/**",
"apps/docs/blog/authors.yml",
"apps/docs/blog/**",
"packages/custom-widgets/src/core/component-catalog.generated.json",
"apps/docs/static/custom-widgets/component-catalog-v1.json"
]
Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 418 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 618 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

@@ -1,162 +0,0 @@
---
authors:
- ajnart
tags: [homarr, update, version, assistant, custom widgets, dashboards, docker]
draft: false
---
# Homarr 2.0.0 - Workshop, Custom Widgets and a rebuilt board
First, thank you.
Many of you used the v2 demo when it was still rough, found bugs and came back to test the next build. Others translated Homarr, answered questions, contributed code or simply kept using it. We would not be shipping this without you.
We are also planning a small batch of **free, limited-edition Homarr swag** for Workshop contributors and a few randomly picked community members. It is not for sale and it is not for profit. No Homarr merch empire, sorry.
Homarr 2.0 is an upgrade, not a brand-new app wearing the same logo. Here is what actually changed.
![A populated Homarr 2.0 dashboard with compact widgets, an Assistant, and an application rail](./img/homarr-v2-dashboard.webp)
<!-- truncate -->
:::warning Back up before upgrading
Create a [Homarr backup](/docs/management/backup) first. Do not test a prerelease image on your only production volume. Backups are boring. Restores are worse.
:::
## Custom Widgets v2 and Workshop
### Build almost any widget
**This is the big one.** We think it is the most important Homarr update we have made.
Custom Widgets v2 lets you build widgets from JSX, API requests, actions and typed settings. Write one yourself or describe it to Assistant, then test it in the workbench. We think this is enough to make almost any dashboard widget—even that oddly specific component only three people need. Those three people no longer have to wait for us.
This was partly inspired by [Google's upcoming Create My Widget feature](https://blog.google/products-and-platforms/platforms/android/gemini-intelligence/): describe a widget in normal language and let AI make it. Coding knowledge is optional. Knowing what the API does still helps; AI has not fixed bad documentation. Yet.
![The Custom Widget v2 workbench after a live exchange-rate request successfully renders its preview](./img/custom-widget-workbench.webp)
A `homarr-custom-widget-v2` definition can use several fixed-origin API sources. Credentials are encrypted separately and stay out of exports and Workshop submissions. Actions support `GET`, `POST`, `PUT`, `PATCH` and `DELETE`; requests have host checks, timeouts, size limits, permissions and rate limits. Custom Widgets remain **Beta** in 2.0.
### Share widgets and Custom CSS
**Workshop is the other half.** Publish and install Custom Widgets, then update, vote on, comment on and report submissions. Workshop is not only for widgets: you can share your **Custom CSS** there too. Deployment URLs and credentials stay on your Homarr instance.
![The Community Workshop catalog inside Custom Widget management](./img/community-workshop.webp)
:::warning Workshop is moderated
AI makes widgets easy to produce. It does not make the first thing it coughs up worth publishing. **Do not dump untested slop into Workshop.** Test it, add a useful screenshot and explain its setup. Workshop is moderated.
:::
### The ultimate Homarr update
We have called this the **"ultimate Homarr update"**. Not because work stops, but because every new service or widget idea no longer needs a full Homarr release; the community can build and share it instead.
## Board editing, rebuilt
### Drag and drop
We threw out the old drag-and-drop system and rebuilt it on [`dnd-kit`](https://dndkit.com/). Moves and eight-direction resizes are transactional: Homarr previews the result, checks collisions and only saves a valid layout. Bad moves roll back instead of wrecking the board. In short: less fighting with your own dashboard.
![A Calendar tile moving across the rebuilt Homarr 2.0 board grid](./img/drag-and-drop.gif)
### Goodbye Groups and sections, bonjour Containers
Groups and dynamic sections did nearly the same job, so **Containers replace both**. They hold apps, widgets and other Containers. Hold <kbd>Ctrl</kbd> or <kbd>Command</kbd> to move several items at once.
### One board, several layouts
Boards also have Base and Mobile layouts, plus optional breakpoints. **Reset from Base** creates a Mobile starting point without duplicating the board.
## Homarr Assistant
### Work with live Homarr data
Assistant reads live Homarr data and has tools for boards, apps, integrations, widgets, Docker and supported services. It can answer questions, make approved changes and build Custom Widgets. It follows your permissions: reads can run automatically, while changes require approval by default.
![The Homarr Assistant expanded over a live dashboard with health, exploration, media, and board actions](./img/assistant-control-surface.webp)
Open it with <kbd>Shift</kbd> + <kbd>A</kbd>. <kbd>Cmd</kbd>/<kbd>Ctrl</kbd> + <kbd>/</kbd> searches it.
### The Homarr provider
:::note
Assistant ships with a **"Homarr provider"**. It is free, will never offer paid plans, and is paid for by [Ajnart](https://github.com/ajnart) using the ad revenue from this website.
It only uses zero-data-retention (ZDR) providers. [The provider is open source](https://github.com/homarr-labs/homarr/blob/release/v2/apps/workshop/homarr_provider.go), and neither prompts nor responses are logged. We are not selling your data. Pinky promise.
:::
### External assistants through MCP
External assistants can use the same permission-checked tools through `/api/mcp`, with an API key or OAuth 2.1 with PKCE.
## The rest (there is a lot)
### Brand-new onboarding
A six-stage setup studio handles the first admin, core settings, Docker discovery, apps, integrations and the first board.
![The Homarr 2.0 setup studio on its Essentials step, with all six setup stages shown](./img/onboarding-studio.webp)
### Board switcher
Boards now live in a searchable gallery with keyboard navigation. Open it from the header or press <kbd>Shift</kbd> + <kbd>C</kbd>.
![A visual board card inside the searchable Homarr board switcher](./img/board-switcher.webp)
### Command menu and keybinds
<kbd>Cmd</kbd>/<kbd>Ctrl</kbd> + <kbd>K</kbd> finds resources, pages, settings, users, groups and commands. More
keybinds cover Assistant, editing, selection and advanced widget views.
### Customizable header
Pick which buttons appear, put them on the left, center or right, and change their order.
![The Header studio showing left, center, and right zones plus available controls](./img/header-studio.webp)
### Instance branding
Change the name, logo, favicon, colors, corner styles and login page. Global CSS applies instance-wide; board CSS can override it. Only give that power to admins you trust. CSS remains CSS.
![Instance branding controls for application identity, palette, corner style, and global CSS](./img/instance-branding.webp)
### Widgets
Add content without leaving the board. Settings have live previews, and supported widgets have advanced views. Weather and Clock were redesigned; Air Quality, Countdown and Timer are new.
### Docker and Podman
`DOCKER_ENDPOINTS` supports several hosts. Assisted setup and container labels can create apps, integrations, Containers, widgets and health checks.
![Assisted Docker setup listing discovered services that can become apps or integrations](./img/docker-assisted-setup.webp)
Homepage-compatible labels work too.
### Permissions
Groups use a new Viewer, Editor and Admin matrix. The API, Assistant and MCP follow the same rules. LDAP can create users on first sign-in; OIDC can link verified accounts and synchronize groups.
### Performance
Board startup work runs in parallel, heavy screens load when opened, and identical integration requests share work instead of doing it twice.
## Before upgrading
- Keep a backup and use the final release image, not a prerelease tag.
- Legacy Custom JSX definitions enter a migration state. Homarr keeps encrypted credentials and an archived copy until the v2 definition validates.
- Mobile layouts place rail content in the main flow instead of displaying rails.
- Custom Widgets remain Beta.
- Assistant and MCP do not bypass permissions.
## Links
- Try the [Homarr 2.0 preview](https://app-v2.preview.homarr.dev).
- Follow the [v2 release pull request](https://github.com/homarr-labs/homarr/pull/6545) and [public beta thread](https://github.com/homarr-labs/homarr/issues/6600).
- Compare [v1.76.2 with the release/v2 branch](https://github.com/homarr-labs/homarr/compare/v1.76.2...release/v2).
- Read the guides for [Boards](/docs/management/boards), [Custom Widgets](/docs/management/custom-widgets), [Assistant](/docs/management/assistant), [MCP](/docs/management/mcp) and [Docker labels](/docs/integrations/docker-labels).
Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 393 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 348 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 414 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

+1 -25
View File
@@ -14,31 +14,7 @@ docker exec -it <container-id> /bin/bash
homarr <command>
```
### QNAP Container Station
1. Open **Container Station**.
![Container Station in the QNAP application menu](./img/334116069-e7ec4726-3df7-4dc6-a3e0-b745e7eef17f.png)
2. Select **Containers**.
![Containers menu in QNAP Container Station](./img/334116205-aa09ce40-0701-4f7d-b9f9-b431b9d207c5.png)
3. Select the Homarr container.
![Homarr container selected in QNAP Container Station](./img/334116303-714b5e07-b9de-4ec0-a2bc-def7ee38750f.png)
4. Select **Execute**.
![Execute action for the Homarr container in QNAP Container Station](./img/334116411-df1482f9-3aa9-4686-b77f-e0dca09e60b2.png)
5. Select `/bin/bash`, then select **Execute**.
![Bash selected in the QNAP execute console](./img/334116518-e1ddb515-99c7-4159-81bf-76e889d1e808.png)
6. Run `homarr <command>` in the console.
![Bash console open for Homarr in QNAP Container Station](./img/334116642-08c4f204-5cd0-4e4e-a785-0142e1b279ca.png)
In QNAP Container Station, open the Homarr container's **Execute** action and select `/bin/bash`.
See the pages in this section for available commands.
@@ -15,10 +15,6 @@ 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
+2 -3
View File
@@ -16,8 +16,7 @@ export const bookmarksWidget: WidgetDefinition = {
},
{
name: "Layout",
description:
"How bookmarks use the available widget space. Layouts stay compact and reduce card size when needed.",
description: "How bookmarks use the available widget space.",
values: { type: "select", options: ["Adaptive", "Vertical", "Horizontal", "Grid", "Compact grid", "Icons"] },
defaultValue: "Adaptive",
},
@@ -29,7 +28,7 @@ export const bookmarksWidget: WidgetDefinition = {
},
{
name: "Card spacing",
description: "Space between bookmark cards. Horizontal, compact-grid, and icon-only layouts cap larger gaps.",
description: "Space between bookmark cards.",
values: { type: "select", options: ["Extra small", "Small", "Medium", "Large", "Extra large"] },
defaultValue: "Extra small",
},
@@ -41,7 +41,6 @@ export default function BookmarksWidget({
width,
height,
displayMode,
displayScale = 1,
}: WidgetComponentProps<"bookmarks">) {
const t = useI18n("widget.bookmarks");
const board = useRequiredBoard();
@@ -67,16 +66,6 @@ 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) => {
@@ -106,13 +95,12 @@ export default function BookmarksWidget({
() =>
getBookmarkDisplayPlan({
advanced,
gap: bookmarkSpacingPixels[options.spacing],
height: contentHeight,
height: options.title.length > 0 ? height - 36 : height,
itemCount: data.length,
layout: options.layout,
width: layoutWidth,
width,
}),
[advanced, contentHeight, data.length, layoutWidth, options.layout, options.spacing],
[advanced, data.length, height, options.layout, options.title.length, width],
);
const cardDisplay = getBookmarkCardDisplay({
@@ -143,10 +131,8 @@ export default function BookmarksWidget({
if (appIds.length > 0 && isInitialWidgetQueryPending(appsQuery)) return <WidgetQueryLoadingState />;
const isTight = responsiveHeight < 120;
return (
<Stack h="100%" mih={0} gap={isTight ? 6 : "sm"} p={isTight ? 6 : "sm"}>
<Stack h="100%" mih={0} gap={height < 120 ? 6 : "sm"} p={height < 120 ? 6 : "sm"}>
{options.title.length > 0 ? (
<Text fz={11} fw={600} px={2} lh={1.2} lineClamp={1}>
{options.title}
@@ -165,18 +151,14 @@ export default function BookmarksWidget({
</Stack>
</Center>
) : (
<ScrollArea
scrollbars={plan.horizontalScroll ? "x" : "y"}
style={{ flex: 1, minHeight: 0 }}
styles={{ content: { height: "100%" } }}
>
<Box miw="100%" h="100%" pb={2}>
<ScrollArea scrollbars={plan.horizontalScroll ? "x" : "y"} style={{ flex: 1, minHeight: 0 }}>
<Box miw="100%" mih="100%" pb={2}>
{plan.horizontalScroll ? (
<Group gap={plan.itemGap} wrap="nowrap" h="100%" align="center">
<Group gap={options.spacing} wrap="nowrap" mih="100%" align="stretch">
{cards}
</Group>
) : options.grow ? (
<Grid grow columns={plan.columns} gap={plan.itemGap}>
<Grid grow columns={plan.columns} gap={options.spacing}>
{cards.map((card) => (
<Grid.Col key={card.key} span={1}>
{card}
@@ -184,7 +166,7 @@ export default function BookmarksWidget({
))}
</Grid>
) : (
<SimpleGrid cols={plan.columns} spacing={plan.itemGap} verticalSpacing={plan.itemGap}>
<SimpleGrid cols={plan.columns} spacing={options.spacing} verticalSpacing={options.spacing}>
{cards}
</SimpleGrid>
)}
@@ -231,17 +213,6 @@ 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 ? (
@@ -267,7 +238,9 @@ const BookmarkCard = ({
align="center"
flex={1}
>
{showIcon ? <BookmarkAvatar bookmark={bookmark} iconUrl={iconUrl} size={avatarSize} /> : null}
{showIcon ? (
<BookmarkAvatar bookmark={bookmark} iconUrl={iconUrl} size={advanced ? 42 : iconOnly ? 32 : 30} />
) : null}
{!iconOnly ? (
<Stack gap={advanced ? 3 : 0} miw={0} flex={1}>
{showTitle ? (
@@ -331,7 +304,7 @@ const BookmarkCard = ({
aria-label={bookmark.name}
radius={radius}
withBorder={withBorder || variant === "outline"}
p={padding}
p={iconOnly ? 6 : advanced ? "md" : "xs"}
h={height}
w={width}
miw={width}
@@ -398,11 +371,3 @@ const getBookmarkHostname = (href: string): string | undefined => {
return undefined;
}
};
const bookmarkSpacingPixels = {
xs: 10,
sm: 12,
md: 16,
lg: 20,
xl: 32,
} as const;
+15 -111
View File
@@ -6,7 +6,6 @@ export type BookmarkOrientation = "horizontal" | "vertical" | "icon";
export interface BookmarkDisplayPlan {
columns: number;
horizontalScroll: boolean;
itemGap: number;
itemHeight: number;
itemWidth: number;
orientation: BookmarkOrientation;
@@ -108,18 +107,6 @@ 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,
@@ -172,20 +159,11 @@ const heightBreakpoints = [
showHostname: false,
showTitle: true,
},
{
minHeight: 60,
columnItemHeight: 48,
itemHeight: 48,
rowItemHeight: 56,
orientation: "icon",
showHostname: false,
showTitle: false,
},
{
minHeight: 0,
columnItemHeight: 32,
itemHeight: 32,
rowItemHeight: 40,
columnItemHeight: 48,
itemHeight: 56,
rowItemHeight: 56,
orientation: "icon",
showHostname: false,
showTitle: false,
@@ -194,14 +172,12 @@ const heightBreakpoints = [
export const getBookmarkDisplayPlan = ({
advanced,
gap = 0,
height,
itemCount,
layout,
width,
}: {
advanced: boolean;
gap?: number;
height: number;
itemCount: number;
layout: BookmarkLayout;
@@ -218,7 +194,6 @@ export const getBookmarkDisplayPlan = ({
return {
columns: Math.min(count, widthSettings.advancedColumns),
horizontalScroll: false,
itemGap: gap,
itemHeight: 104,
itemWidth: 260,
orientation: "horizontal",
@@ -228,41 +203,28 @@ export const getBookmarkDisplayPlan = ({
}
if (layout === "row") {
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;
let orientation: BookmarkOrientation = "vertical";
if (!heightSettings.showTitle) orientation = "icon";
return {
columns: count,
horizontalScroll: true,
itemGap: Math.min(gap, 8),
itemHeight: Math.min(heightSettings.rowItemHeight, 64),
itemWidth,
itemHeight: heightSettings.rowItemHeight,
itemWidth: widthSettings.rowItemWidth,
orientation,
showHostname: widthSettings.showHostname && heightSettings.showHostname,
showTitle: widthSettings.showTitle,
showTitle: widthSettings.showTitle && heightSettings.showTitle,
};
}
if (layout === "column") {
const densitySettings = getBookmarkDensitySettings({
columns: 1,
gap,
height: normalizedHeight,
heightProperty: "columnItemHeight",
itemCount: count,
preferredSettings: heightSettings,
});
return {
columns: 1,
horizontalScroll: false,
itemGap: getBookmarkDensityGap(gap, densitySettings),
itemHeight: densitySettings.columnItemHeight,
itemHeight: heightSettings.columnItemHeight,
itemWidth: width,
orientation: "horizontal",
showHostname: widthSettings.showHostname && densitySettings.showHostname,
showTitle: widthSettings.showTitle && densitySettings.showTitle,
showHostname: widthSettings.showHostname && heightSettings.showHostname,
showTitle: widthSettings.showTitle && heightSettings.showTitle,
};
}
@@ -270,7 +232,6 @@ export const getBookmarkDisplayPlan = ({
return {
columns: Math.min(count, widthSettings.iconColumns),
horizontalScroll: false,
itemGap: Math.min(gap, 8),
itemHeight: 56,
itemWidth: 56,
orientation: "icon",
@@ -283,7 +244,6 @@ export const getBookmarkDisplayPlan = ({
return {
columns: Math.min(count, widthSettings.compactColumns),
horizontalScroll: false,
itemGap: Math.min(gap, 8),
itemHeight: 56,
itemWidth: widthSettings.rowItemWidth,
orientation: "horizontal",
@@ -305,69 +265,13 @@ 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: visibleColumns,
columns: Math.min(count, columns),
horizontalScroll: false,
itemGap: getBookmarkDensityGap(gap, densitySettings),
itemHeight: densitySettings.itemHeight,
itemHeight: heightSettings.itemHeight,
itemWidth: widthSettings.rowItemWidth,
orientation,
showHostname,
showTitle,
showHostname: orientation !== "icon" && widthSettings.showHostname && heightSettings.showHostname,
showTitle: orientation !== "icon" && widthSettings.showTitle && heightSettings.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);
};