From a7630b7d6d619f7e757efce7462a4f92732829c6 Mon Sep 17 00:00:00 2001 From: ajnart Date: Mon, 24 Aug 2026 18:39:32 +0200 Subject: [PATCH] feat: add configurable header logo display --- .../app/[locale]/boards/_layout-creator.tsx | 3 +- .../_components/header-composer.module.css | 40 ++++++++++-- .../general/_components/header-composer.tsx | 65 +++++++++++++++++-- apps/nextjs/src/components/layout/header.tsx | 5 +- .../header/configurable-header.module.css | 45 ++++--------- .../layout/header/configurable-header.tsx | 20 ++++-- .../layout/header/header-logo.module.css | 22 +++++++ .../components/layout/header/header-logo.tsx | 30 +++++++++ .../src/components/layout/logo/board-logo.tsx | 2 +- .../components/layout/logo/homarr-logo.tsx | 5 +- .../src/components/layout/logo/logo.tsx | 12 ++-- packages/api/src/router/user.ts | 2 +- ...48_add_header_preferences_and_branding.sql | 2 +- .../migrations/mysql/meta/0048_snapshot.json | 2 +- ...14_add_header_preferences_and_branding.sql | 2 +- .../postgresql/meta/0014_snapshot.json | 2 +- ...46_add_header_preferences_and_branding.sql | 2 +- .../migrations/sqlite/meta/0046_snapshot.json | 2 +- packages/translation/src/lang/en.json | 4 ++ packages/validation/src/header-preferences.ts | 5 ++ packages/validation/src/user.ts | 2 + 21 files changed, 209 insertions(+), 65 deletions(-) create mode 100644 apps/nextjs/src/components/layout/header/header-logo.module.css create mode 100644 apps/nextjs/src/components/layout/header/header-logo.tsx diff --git a/apps/nextjs/src/app/[locale]/boards/_layout-creator.tsx b/apps/nextjs/src/app/[locale]/boards/_layout-creator.tsx index 195f01f13..b23b0b081 100644 --- a/apps/nextjs/src/app/[locale]/boards/_layout-creator.tsx +++ b/apps/nextjs/src/app/[locale]/boards/_layout-creator.tsx @@ -13,7 +13,7 @@ import { ModalProvider } from "@homarr/modals"; import { MainHeader } from "~/components/layout/header"; import { appShellLogoHeight } from "~/components/layout/constants"; -import { BoardLogo } from "~/components/layout/logo/board-logo"; +import { BoardLogo, BoardLogoWithTitle } from "~/components/layout/logo/board-logo"; import { ClientShell } from "~/components/layout/shell"; import { BoardTourGate } from "~/components/onboarding/board-tour-gate"; import { env } from "~/env"; @@ -111,6 +111,7 @@ export const createBoardLayout = ({ } + logoWithTitle={} actions={headerActions} boardEditAction={headerBoardEditAction} boardSettingsAction={headerBoardSettingsAction} diff --git a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/header-composer.module.css b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/header-composer.module.css index 31819934f..de2ca84c5 100644 --- a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/header-composer.module.css +++ b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/header-composer.module.css @@ -25,7 +25,7 @@ .device { position: relative; - width: min(100%, 31.2rem); + width: min(100%, 34rem); min-height: 18rem; overflow: hidden; background: var(--mantine-color-body); @@ -148,7 +148,7 @@ .itemButton[data-wide="true"], .dragOverlay[data-wide="true"] { - width: 9rem; + width: 4.75rem; } .itemButton:hover, @@ -176,20 +176,48 @@ place-items: center; width: 100%; min-width: 0; - color: var(--mantine-color-dimmed); + color: var(--mantine-color-text); +} + +.logoPreview { + display: flex; + align-items: center; + justify-content: center; + gap: 0.25rem; + width: 100%; + min-width: 0; +} + +.logoPreview[data-with-title="true"] { + justify-content: flex-start; + padding-inline: 0.375rem; +} + +.logoPreview span { + min-width: 0; + overflow: hidden; + font-size: var(--mantine-font-size-xs); + font-weight: 600; + line-height: 1; + text-overflow: ellipsis; + white-space: nowrap; } .searchPreview[data-input="true"] { grid-template-columns: auto minmax(0, 1fr); - gap: var(--mantine-spacing-xs); - padding-inline: var(--mantine-spacing-sm); - font-size: var(--mantine-font-size-sm); + gap: 0.25rem; + padding-inline: 0.375rem; + font-size: var(--mantine-font-size-xs); text-align: start; } .searchPreview span { + width: 100%; + min-width: 0; overflow: hidden; + color: var(--mantine-color-dimmed); text-overflow: ellipsis; + white-space: nowrap; } .hiddenPreview { diff --git a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/header-composer.tsx b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/header-composer.tsx index 38dd43548..cdff2b4b6 100644 --- a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/header-composer.tsx +++ b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/header-composer.tsx @@ -39,6 +39,7 @@ import { } from "@tabler/icons-react"; import type { BoardPreviewData } from "@homarr/boards/layout-preview"; +import { useSettings } from "@homarr/settings"; import { useI18n } from "@homarr/translation/client"; import type { HeaderBuiltinItemId, @@ -97,6 +98,7 @@ const headerGuideZones = ["left", "center", "right"] as const; export const HeaderComposer = ({ value, onChange, boards, homeBoardId }: HeaderComposerProps) => { const [draggedItemKey, setDraggedItemKey] = useState(null); const [selectedItemKey, setSelectedItemKey] = useState(null); + const { branding } = useSettings(); const t = useI18n("management.page.user.setting.general.header"); const boardsById = useMemo(() => new Map(boards.map((board) => [board.id, board])), [boards]); const activeItems = getHeaderItems(value.zones); @@ -217,12 +219,14 @@ export const HeaderComposer = ({ value, onChange, boards, homeBoardId }: HeaderC index={index} label={itemLabel(item)} searchDisplay={value.searchDisplay} + logoDisplay={value.logoDisplay} + logoTitle={branding.appName} boardLogo={ item.type === "board" ? (boardsById.get(item.boardId)?.logoImageUrl ?? null) : null } configureLabel={t("action.configure", { item: itemLabel(item) })} selected={selectedItemKey === getHeaderItemKey(item)} - onSelect={toggleSelectedItem} + onSelect={toggleSelectedItem} /> ))} @@ -236,6 +240,8 @@ export const HeaderComposer = ({ value, onChange, boards, homeBoardId }: HeaderC label={itemLabel(draggedItem)} compact={false} searchDisplay={value.searchDisplay} + logoDisplay={value.logoDisplay} + logoTitle={branding.appName} boardLogo={ draggedItem.type === "board" ? boardsById.get(draggedItem.boardId)?.logoImageUrl : null } @@ -287,6 +293,19 @@ export const HeaderComposer = ({ value, onChange, boards, homeBoardId }: HeaderC ]} /> ) : null} + {selectedItem.type === "builtin" && selectedItem.id === "logo" ? ( + + onChange({ ...value, logoDisplay: logoDisplay as HeaderPreferences["logoDisplay"] }) + } + data={[ + { value: "logo", label: t("configuration.logoDisplay.logo") }, + { value: "logoAndText", label: t("configuration.logoDisplay.logoAndText") }, + ]} + /> + ) : null} {!isRequiredHeaderItem(selectedItem) ? ( @@ -470,6 +498,8 @@ const HeaderItemPreview = ({ label, compact, searchDisplay, + logoDisplay, + logoTitle, boardLogo, overlay = false, }: { @@ -477,11 +507,21 @@ const HeaderItemPreview = ({ label: string; compact: boolean; searchDisplay: HeaderPreferences["searchDisplay"]; + logoDisplay: HeaderPreferences["logoDisplay"]; + logoTitle: string; boardLogo: string | null | undefined; overlay?: boolean; }) => { const content = ( - + ); if (!overlay) return content; @@ -490,7 +530,11 @@ const HeaderItemPreview = ({ className={classes.dragOverlay} data-kind={item.type === "builtin" ? item.id : "board"} data-wide={ - item.type === "builtin" && item.id === "search" && searchDisplay === "input" && !compact ? true : undefined + (item.type === "builtin" && + !compact && + ((item.id === "search" && searchDisplay === "input") || + (item.id === "logo" && logoDisplay === "logoAndText"))) || + undefined } > {content} @@ -503,12 +547,16 @@ const PreviewItem = ({ label, compact, searchDisplay, + logoDisplay, + logoTitle, boardLogo, }: { item: HeaderItem; label: string; compact: boolean; searchDisplay: HeaderPreferences["searchDisplay"]; + logoDisplay: HeaderPreferences["logoDisplay"]; + logoTitle: string; boardLogo: string | null | undefined; }) => { if (item.type === "board") { @@ -519,7 +567,14 @@ const PreviewItem = ({ ); } - if (item.id === "logo") return ; + if (item.id === "logo") { + return ( +
+ + {logoDisplay === "logoAndText" ? {logoTitle} : null} +
+ ); + } if (item.id === "search") { const showInput = searchDisplay === "input" && !compact; diff --git a/apps/nextjs/src/components/layout/header.tsx b/apps/nextjs/src/components/layout/header.tsx index d97615bbb..4f268039e 100644 --- a/apps/nextjs/src/components/layout/header.tsx +++ b/apps/nextjs/src/components/layout/header.tsx @@ -5,10 +5,11 @@ import { env } from "@homarr/docker/env"; import { CurrentUserAvatar } from "~/components/user-avatar"; import { appShellLogoHeight } from "./constants"; import { ConfigurableHeader } from "./header/configurable-header"; -import { HomarrLogo } from "./logo/homarr-logo"; +import { HomarrLogo, HomarrLogoWithTitle } from "./logo/homarr-logo"; interface Props { logo?: ReactNode; + logoWithTitle?: ReactNode; actions?: ReactNode; boardEditAction?: ReactNode; boardSettingsAction?: ReactNode; @@ -17,6 +18,7 @@ interface Props { export const MainHeader = async ({ logo, + logoWithTitle, actions, boardEditAction, boardSettingsAction, @@ -29,6 +31,7 @@ export const MainHeader = async ({ return ( } + logoWithTitle={logoWithTitle ?? } actions={actions} boardEditAction={boardEditAction} boardSettingsAction={boardSettingsAction} diff --git a/apps/nextjs/src/components/layout/header/configurable-header.module.css b/apps/nextjs/src/components/layout/header/configurable-header.module.css index 7dd45702e..692adbad2 100644 --- a/apps/nextjs/src/components/layout/header/configurable-header.module.css +++ b/apps/nextjs/src/components/layout/header/configurable-header.module.css @@ -37,39 +37,6 @@ justify-content: flex-end; } -.logo { - display: grid; - place-items: center; - min-width: 0; - min-height: 0; - width: auto; - height: calc(var(--app-shell-header-height) - 0.5rem); - max-height: calc(var(--app-shell-header-height) - 0.5rem); - max-width: min(12rem, 28vw); - flex: 0 1 auto; - overflow: hidden; - border-radius: var(--mantine-radius-md); -} - -.logo img { - display: block; - width: auto; - height: auto; - max-width: 100%; - max-height: calc(var(--app-shell-header-height) - 0.5rem); - object-fit: contain; -} - -.logo > * { - flex-wrap: nowrap; -} - -.logo h1, -.logo h2, -.logo h3 { - display: none; -} - .search { display: flex; min-width: 0; @@ -156,6 +123,18 @@ } } +@media (hover: none), (pointer: coarse) { + .floatingControls { + opacity: 1; + pointer-events: auto; + transform: none; + } + + .floatingControlsCorner { + display: none; + } +} + @media (prefers-reduced-motion: reduce) { .floatingControls { transition: none; diff --git a/apps/nextjs/src/components/layout/header/configurable-header.tsx b/apps/nextjs/src/components/layout/header/configurable-header.tsx index b9946f087..5d4bf0ef3 100644 --- a/apps/nextjs/src/components/layout/header/configurable-header.tsx +++ b/apps/nextjs/src/components/layout/header/configurable-header.tsx @@ -9,7 +9,6 @@ import { Indicator, Loader, Tooltip, - UnstyledButton, useMantineColorScheme, } from "@mantine/core"; import { @@ -28,7 +27,6 @@ import { invariantTechnicalLabels } from "@homarr/definitions"; import { useModalAction } from "@homarr/modals"; import { useSettings } from "@homarr/settings"; import { useI18n } from "@homarr/translation/client"; -import { Link } from "@homarr/ui"; import type { HeaderItem } from "@homarr/validation/user"; import { getHeaderItemKey, getHeaderItems } from "@homarr/validation/user"; @@ -38,6 +36,7 @@ import type { BoardSwitcherControls } from "~/components/board/board-switcher"; import { ClientBurger } from "./burger"; import { HeaderButton } from "./button"; import { DockerQuickAccessModal } from "./docker-quick-access-modal"; +import { HeaderLogo } from "./header-logo"; import { LazySpotlight } from "./lazy-spotlight"; import { DesktopSearchInput, MobileSearchButton } from "./search"; import { TourTarget } from "./tour-target"; @@ -46,6 +45,7 @@ import classes from "./configurable-header.module.css"; interface ConfigurableHeaderProps { logo: ReactNode; + logoWithTitle: ReactNode; actions?: ReactNode; boardEditAction?: ReactNode; boardSettingsAction?: ReactNode; @@ -63,6 +63,7 @@ const floatingControlsDismissDelayMs = 900; export const ConfigurableHeader = ({ logo, + logoWithTitle, actions, boardEditAction, boardSettingsAction, @@ -94,6 +95,7 @@ export const ConfigurableHeader = ({ key={getHeaderItemKey(item)} item={item} logo={logo} + logoWithTitle={logoWithTitle} boardEditAction={boardEditAction} boardSettingsAction={boardSettingsAction} avatar={avatar} @@ -102,6 +104,7 @@ export const ConfigurableHeader = ({ isDockerEnabled={isDockerEnabled} board={item.type === "board" ? boardsById.get(item.boardId) : undefined} searchDisplay={headerPreferences.searchDisplay} + logoDisplay={headerPreferences.logoDisplay} boardSwitcher={boardSwitcher} assistant={assistant} openDockerModal={openDockerModal} @@ -236,6 +239,7 @@ const FloatingHeaderControls = ({ children }: { children: ReactNode }) => { interface HeaderItemProps { item: HeaderItem; logo: ReactNode; + logoWithTitle: ReactNode; boardEditAction: ReactNode; boardSettingsAction: ReactNode; avatar: ReactNode; @@ -244,6 +248,7 @@ interface HeaderItemProps { isDockerEnabled: boolean; board: HeaderBoard | undefined; searchDisplay: "input" | "icon"; + logoDisplay: "logo" | "logoAndText"; boardSwitcher: BoardSwitcherControls; assistant: ReturnType; openDockerModal: () => void; @@ -255,6 +260,7 @@ interface HeaderItemProps { const HeaderItem = ({ item, logo, + logoWithTitle, boardEditAction, boardSettingsAction, avatar, @@ -263,6 +269,7 @@ const HeaderItem = ({ isDockerEnabled, board, searchDisplay, + logoDisplay, boardSwitcher, assistant, openDockerModal, @@ -293,9 +300,12 @@ const HeaderItem = ({ if (item.id === "logo") { return ( - - {logo} - + ); } diff --git a/apps/nextjs/src/components/layout/header/header-logo.module.css b/apps/nextjs/src/components/layout/header/header-logo.module.css new file mode 100644 index 000000000..f5185ea95 --- /dev/null +++ b/apps/nextjs/src/components/layout/header/header-logo.module.css @@ -0,0 +1,22 @@ +.root { + display: flex; + align-items: center; + flex: 0 0 auto; + border-radius: var(--mantine-radius-md); +} + +.root[data-display="logo"] { + justify-content: center; + width: calc(var(--app-shell-header-height) - 0.5rem); + height: calc(var(--app-shell-header-height) - 0.5rem); + overflow: hidden; +} + +.root[data-display="logo"] img { + display: block; + width: auto; + height: auto; + max-width: 100%; + max-height: calc(var(--app-shell-header-height) - 0.5rem); + object-fit: contain; +} diff --git a/apps/nextjs/src/components/layout/header/header-logo.tsx b/apps/nextjs/src/components/layout/header/header-logo.tsx new file mode 100644 index 000000000..f68924618 --- /dev/null +++ b/apps/nextjs/src/components/layout/header/header-logo.tsx @@ -0,0 +1,30 @@ +import type { ReactNode } from "react"; +import { UnstyledButton } from "@mantine/core"; + +import { Link } from "@homarr/ui"; + +import classes from "./header-logo.module.css"; + +interface HeaderLogoProps { + display: "logo" | "logoAndText"; + logo: ReactNode; + logoWithTitle: ReactNode; + label: string; +} + +export const HeaderLogo = ({ display, logo, logoWithTitle, label }: HeaderLogoProps) => { + let content = logo; + if (display === "logoAndText") content = logoWithTitle; + + return ( + + {content} + + ); +}; diff --git a/apps/nextjs/src/components/layout/logo/board-logo.tsx b/apps/nextjs/src/components/layout/logo/board-logo.tsx index e9bfe3d62..dfa9daf28 100644 --- a/apps/nextjs/src/components/layout/logo/board-logo.tsx +++ b/apps/nextjs/src/components/layout/logo/board-logo.tsx @@ -15,7 +15,7 @@ const useImageOptions = () => { const board = useRequiredBoard(); const { branding } = useSettings(); return { - src: board.logoImageUrl ?? branding.logoImageUrl ?? homarrLogoPath, + src: board.logoImageUrl?.trim() || branding.logoImageUrl?.trim() || homarrLogoPath, alt: "Board logo", shouldUseNextImage: false, }; diff --git a/apps/nextjs/src/components/layout/logo/homarr-logo.tsx b/apps/nextjs/src/components/layout/logo/homarr-logo.tsx index c18f08439..30aad1858 100644 --- a/apps/nextjs/src/components/layout/logo/homarr-logo.tsx +++ b/apps/nextjs/src/components/layout/logo/homarr-logo.tsx @@ -12,12 +12,13 @@ interface LogoProps { const useBrandLogo = () => { const { branding } = useSettings(); + const configuredLogoUrl = branding.logoImageUrl?.trim(); return { title: branding.appName, image: { - src: branding.logoImageUrl ?? homarrLogoPath, + src: configuredLogoUrl || homarrLogoPath, alt: `${branding.appName} logo`, - shouldUseNextImage: branding.logoImageUrl === null, + shouldUseNextImage: !configuredLogoUrl, }, }; }; diff --git a/apps/nextjs/src/components/layout/logo/logo.tsx b/apps/nextjs/src/components/layout/logo/logo.tsx index 36eaba57b..a2a541295 100644 --- a/apps/nextjs/src/components/layout/logo/logo.tsx +++ b/apps/nextjs/src/components/layout/logo/logo.tsx @@ -9,14 +9,18 @@ interface LogoProps { shouldUseNextImage?: boolean; } -export const Logo = ({ size = 60, shouldUseNextImage = false, src, alt }: LogoProps) => - shouldUseNextImage ? ( - {alt} +export const Logo = ({ size = 60, shouldUseNextImage = false, src, alt }: LogoProps) => { + const normalizedSrc = src.trim(); + if (normalizedSrc.length === 0) return null; + + return shouldUseNextImage ? ( + {alt} ) : ( // we only want to use next/image for logos that we are sure will be preloaded and are allowed // eslint-disable-next-line @next/next/no-img-element - {alt} + {alt} ); +}; const logoWithTitleSizes = { lg: { logoSize: 48, titleOrder: 1 }, diff --git a/packages/api/src/router/user.ts b/packages/api/src/router/user.ts index 734121e64..17c2bcd4c 100644 --- a/packages/api/src/router/user.ts +++ b/packages/api/src/router/user.ts @@ -737,7 +737,7 @@ export const userRouter = createTRPCRouter({ mcp: { enabled: true, description: - "Update the header layout preferences of a user. REQUIRED: id (user ID), headerPreferences (zones for left, center and right, visible flag and searchDisplay). Admins can change any user; other users can only change their own. Board shortcut items must reference boards the target user can view", + "Update the header layout preferences of a user. REQUIRED: id (user ID), headerPreferences (zones for left, center and right, visible flag, searchDisplay and logoDisplay). Admins can change any user; other users can only change their own. Board shortcut items must reference boards the target user can view", }, }) .input(z.object({ id: z.string(), headerPreferences: headerPreferencesMutationSchema })) diff --git a/packages/db/migrations/mysql/0048_add_header_preferences_and_branding.sql b/packages/db/migrations/mysql/0048_add_header_preferences_and_branding.sql index c49b86ea0..f9f1439a3 100644 --- a/packages/db/migrations/mysql/0048_add_header_preferences_and_branding.sql +++ b/packages/db/migrations/mysql/0048_add_header_preferences_and_branding.sql @@ -1,4 +1,4 @@ -ALTER TABLE `user` ADD `header_preferences` text DEFAULT ('{"version":3,"visible":true,"searchDisplay":"input","zones":{"left":[{"type":"builtin","id":"logo"}],"center":[{"type":"builtin","id":"search"}],"right":[{"type":"builtin","id":"boardEdit"},{"type":"builtin","id":"boardSettings"},{"type":"builtin","id":"user"}]}}') NOT NULL; +ALTER TABLE `user` ADD `header_preferences` text DEFAULT ('{"version":3,"visible":true,"searchDisplay":"input","logoDisplay":"logo","zones":{"left":[{"type":"builtin","id":"logo"}],"center":[{"type":"builtin","id":"search"}],"right":[{"type":"builtin","id":"boardEdit"},{"type":"builtin","id":"boardSettings"},{"type":"builtin","id":"user"}]}}') NOT NULL; --> statement-breakpoint INSERT IGNORE INTO `serverSetting` (`setting_key`, `value`) VALUES ('branding', '{"json":{"appName":"Homarr","greeting":"","logoImageUrl":null,"faviconImageUrl":null,"primaryColor":"#fa5252","secondaryColor":"#fd7e14","lockPrimaryColor":false,"signInBackgroundImageUrl":null,"signInBackgroundOverlay":0.55,"authBranding":{"showAppName":true,"showLogo":true,"showGreeting":true},"defaultRadius":"md"}}'); diff --git a/packages/db/migrations/mysql/meta/0048_snapshot.json b/packages/db/migrations/mysql/meta/0048_snapshot.json index 439702c24..3edeaffc4 100644 --- a/packages/db/migrations/mysql/meta/0048_snapshot.json +++ b/packages/db/migrations/mysql/meta/0048_snapshot.json @@ -2620,7 +2620,7 @@ "primaryKey": false, "notNull": true, "autoincrement": false, - "default": "('{\"version\":3,\"visible\":true,\"searchDisplay\":\"input\",\"zones\":{\"left\":[{\"type\":\"builtin\",\"id\":\"logo\"}],\"center\":[{\"type\":\"builtin\",\"id\":\"search\"}],\"right\":[{\"type\":\"builtin\",\"id\":\"boardEdit\"},{\"type\":\"builtin\",\"id\":\"boardSettings\"},{\"type\":\"builtin\",\"id\":\"user\"}]}}')" + "default": "('{\"version\":3,\"visible\":true,\"searchDisplay\":\"input\",\"logoDisplay\":\"logo\",\"zones\":{\"left\":[{\"type\":\"builtin\",\"id\":\"logo\"}],\"center\":[{\"type\":\"builtin\",\"id\":\"search\"}],\"right\":[{\"type\":\"builtin\",\"id\":\"boardEdit\"},{\"type\":\"builtin\",\"id\":\"boardSettings\"},{\"type\":\"builtin\",\"id\":\"user\"}]}}')" }, "completed_manage_tour": { "name": "completed_manage_tour", diff --git a/packages/db/migrations/postgresql/0014_add_header_preferences_and_branding.sql b/packages/db/migrations/postgresql/0014_add_header_preferences_and_branding.sql index 8f75ee391..3325bab84 100644 --- a/packages/db/migrations/postgresql/0014_add_header_preferences_and_branding.sql +++ b/packages/db/migrations/postgresql/0014_add_header_preferences_and_branding.sql @@ -1,4 +1,4 @@ -ALTER TABLE "user" ADD COLUMN "header_preferences" text DEFAULT '{"version":3,"visible":true,"searchDisplay":"input","zones":{"left":[{"type":"builtin","id":"logo"}],"center":[{"type":"builtin","id":"search"}],"right":[{"type":"builtin","id":"boardEdit"},{"type":"builtin","id":"boardSettings"},{"type":"builtin","id":"user"}]}}' NOT NULL; +ALTER TABLE "user" ADD COLUMN "header_preferences" text DEFAULT '{"version":3,"visible":true,"searchDisplay":"input","logoDisplay":"logo","zones":{"left":[{"type":"builtin","id":"logo"}],"center":[{"type":"builtin","id":"search"}],"right":[{"type":"builtin","id":"boardEdit"},{"type":"builtin","id":"boardSettings"},{"type":"builtin","id":"user"}]}}' NOT NULL; --> statement-breakpoint INSERT INTO "serverSetting" ("setting_key", "value") VALUES ('branding', '{"json":{"appName":"Homarr","greeting":"","logoImageUrl":null,"faviconImageUrl":null,"primaryColor":"#fa5252","secondaryColor":"#fd7e14","lockPrimaryColor":false,"signInBackgroundImageUrl":null,"signInBackgroundOverlay":0.55,"authBranding":{"showAppName":true,"showLogo":true,"showGreeting":true},"defaultRadius":"md"}}') diff --git a/packages/db/migrations/postgresql/meta/0014_snapshot.json b/packages/db/migrations/postgresql/meta/0014_snapshot.json index 474506922..8e5cdd87d 100644 --- a/packages/db/migrations/postgresql/meta/0014_snapshot.json +++ b/packages/db/migrations/postgresql/meta/0014_snapshot.json @@ -2485,7 +2485,7 @@ "type": "text", "primaryKey": false, "notNull": true, - "default": "'{\"version\":3,\"visible\":true,\"searchDisplay\":\"input\",\"zones\":{\"left\":[{\"type\":\"builtin\",\"id\":\"logo\"}],\"center\":[{\"type\":\"builtin\",\"id\":\"search\"}],\"right\":[{\"type\":\"builtin\",\"id\":\"boardEdit\"},{\"type\":\"builtin\",\"id\":\"boardSettings\"},{\"type\":\"builtin\",\"id\":\"user\"}]}}'" + "default": "'{\"version\":3,\"visible\":true,\"searchDisplay\":\"input\",\"logoDisplay\":\"logo\",\"zones\":{\"left\":[{\"type\":\"builtin\",\"id\":\"logo\"}],\"center\":[{\"type\":\"builtin\",\"id\":\"search\"}],\"right\":[{\"type\":\"builtin\",\"id\":\"boardEdit\"},{\"type\":\"builtin\",\"id\":\"boardSettings\"},{\"type\":\"builtin\",\"id\":\"user\"}]}}'" }, "completed_manage_tour": { "name": "completed_manage_tour", diff --git a/packages/db/migrations/sqlite/0046_add_header_preferences_and_branding.sql b/packages/db/migrations/sqlite/0046_add_header_preferences_and_branding.sql index 24fddf138..789638134 100644 --- a/packages/db/migrations/sqlite/0046_add_header_preferences_and_branding.sql +++ b/packages/db/migrations/sqlite/0046_add_header_preferences_and_branding.sql @@ -1,4 +1,4 @@ -ALTER TABLE `user` ADD `header_preferences` text DEFAULT '{"version":3,"visible":true,"searchDisplay":"input","zones":{"left":[{"type":"builtin","id":"logo"}],"center":[{"type":"builtin","id":"search"}],"right":[{"type":"builtin","id":"boardEdit"},{"type":"builtin","id":"boardSettings"},{"type":"builtin","id":"user"}]}}' NOT NULL; +ALTER TABLE `user` ADD `header_preferences` text DEFAULT '{"version":3,"visible":true,"searchDisplay":"input","logoDisplay":"logo","zones":{"left":[{"type":"builtin","id":"logo"}],"center":[{"type":"builtin","id":"search"}],"right":[{"type":"builtin","id":"boardEdit"},{"type":"builtin","id":"boardSettings"},{"type":"builtin","id":"user"}]}}' NOT NULL; --> statement-breakpoint INSERT INTO `serverSetting` (`setting_key`, `value`) VALUES ('branding', '{"json":{"appName":"Homarr","greeting":"","logoImageUrl":null,"faviconImageUrl":null,"primaryColor":"#fa5252","secondaryColor":"#fd7e14","lockPrimaryColor":false,"signInBackgroundImageUrl":null,"signInBackgroundOverlay":0.55,"authBranding":{"showAppName":true,"showLogo":true,"showGreeting":true},"defaultRadius":"md"}}') diff --git a/packages/db/migrations/sqlite/meta/0046_snapshot.json b/packages/db/migrations/sqlite/meta/0046_snapshot.json index 2aec9f4ca..bf89440f3 100644 --- a/packages/db/migrations/sqlite/meta/0046_snapshot.json +++ b/packages/db/migrations/sqlite/meta/0046_snapshot.json @@ -2551,7 +2551,7 @@ "primaryKey": false, "notNull": true, "autoincrement": false, - "default": "'{\"version\":3,\"visible\":true,\"searchDisplay\":\"input\",\"zones\":{\"left\":[{\"type\":\"builtin\",\"id\":\"logo\"}],\"center\":[{\"type\":\"builtin\",\"id\":\"search\"}],\"right\":[{\"type\":\"builtin\",\"id\":\"boardEdit\"},{\"type\":\"builtin\",\"id\":\"boardSettings\"},{\"type\":\"builtin\",\"id\":\"user\"}]}}'" + "default": "'{\"version\":3,\"visible\":true,\"searchDisplay\":\"input\",\"logoDisplay\":\"logo\",\"zones\":{\"left\":[{\"type\":\"builtin\",\"id\":\"logo\"}],\"center\":[{\"type\":\"builtin\",\"id\":\"search\"}],\"right\":[{\"type\":\"builtin\",\"id\":\"boardEdit\"},{\"type\":\"builtin\",\"id\":\"boardSettings\"},{\"type\":\"builtin\",\"id\":\"user\"}]}}'" }, "completed_manage_tour": { "name": "completed_manage_tour", diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json index e0b487f02..59e09e437 100644 --- a/packages/translation/src/lang/en.json +++ b/packages/translation/src/lang/en.json @@ -5725,6 +5725,10 @@ "searchDisplay": { "input": "Search bar", "icon": "Icon only" + }, + "logoDisplay": { + "logo": "Logo only", + "logoAndText": "Logo + text" } }, "items": { diff --git a/packages/validation/src/header-preferences.ts b/packages/validation/src/header-preferences.ts index d0f147d2b..e5866a043 100644 --- a/packages/validation/src/header-preferences.ts +++ b/packages/validation/src/header-preferences.ts @@ -26,9 +26,11 @@ export type HeaderPreferenceItemId = HeaderBuiltinItemId; export const headerZoneIds = ["left", "center", "right"] as const; export const headerSearchDisplayValues = ["input", "icon"] as const; +export const headerLogoDisplayValues = ["logo", "logoAndText"] as const; export type HeaderZoneId = (typeof headerZoneIds)[number]; export type HeaderSearchDisplay = (typeof headerSearchDisplayValues)[number]; +export type HeaderLogoDisplay = (typeof headerLogoDisplayValues)[number]; const headerBuiltinItemSchema = z .object({ @@ -102,6 +104,7 @@ export const headerPreferencesSchema = z version: z.literal(headerPreferencesVersion), visible: z.boolean(), searchDisplay: z.enum(headerSearchDisplayValues), + logoDisplay: z.enum(headerLogoDisplayValues).default("logo"), zones: headerZonesSchema, }) .strict(); @@ -151,6 +154,7 @@ const migrateLegacyItems = ( version: headerPreferencesVersion, visible, searchDisplay: "input", + logoDisplay: "logo", zones: { left: zones.left.map(createBuiltinHeaderItem), center: zones.center.map(createBuiltinHeaderItem), @@ -204,6 +208,7 @@ export const defaultHeaderPreferences = { version: headerPreferencesVersion, visible: true, searchDisplay: "input", + logoDisplay: "logo", zones: { left: [createBuiltinHeaderItem("logo")], center: [createBuiltinHeaderItem("search")], diff --git a/packages/validation/src/user.ts b/packages/validation/src/user.ts index ce08bb367..8d8777866 100644 --- a/packages/validation/src/user.ts +++ b/packages/validation/src/user.ts @@ -19,6 +19,7 @@ export { headerBuiltinItemIds, headerItemIds, headerItemSchema, + headerLogoDisplayValues, headerPreferenceItemIds, headerPreferencesMutationSchema, headerPreferencesSchema, @@ -35,6 +36,7 @@ export type { HeaderBuiltinItemId, HeaderItem, HeaderItemId, + HeaderLogoDisplay, HeaderPreferenceItemId, HeaderPreferences, HeaderSearchDisplay,