feat: add configurable header logo display

This commit is contained in:
ajnart committed 2026-08-24 18:39:32 +02:00
1 parent 2dbeb792a7
commit a7630b7d6d
21 files changed
+209 -65

No files matched your search

@@ -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 = <TParams extends Params>({
<ClientShell hasNavigation={false}>
<MainHeader
logo={<BoardLogo size={appShellLogoHeight} />}
logoWithTitle={<BoardLogoWithTitle size="md" hideTitleOnMobile />}
actions={headerActions}
boardEditAction={headerBoardEditAction}
boardSettingsAction={headerBoardSettingsAction}
@@ -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 {
@@ -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<string | null>(null);
const [selectedItemKey, setSelectedItemKey] = useState<string | null>(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}
/>
))}
</HeaderZoneColumn>
@@ -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" ? (
<SegmentedControl
size="xs"
value={value.logoDisplay}
onChange={(logoDisplay) =>
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) ? (
<Button
size="xs"
@@ -403,6 +422,8 @@ interface SortableHeaderItemProps {
index: number;
label: string;
searchDisplay: HeaderPreferences["searchDisplay"];
logoDisplay: HeaderPreferences["logoDisplay"];
logoTitle: string;
boardLogo: string | null;
configureLabel: string;
selected: boolean;
@@ -415,6 +436,8 @@ const SortableHeaderItem = ({
index,
label,
searchDisplay,
logoDisplay,
logoTitle,
boardLogo,
configureLabel,
selected,
@@ -445,7 +468,10 @@ const SortableHeaderItem = ({
className={classes.itemButton}
data-kind={item.type === "builtin" ? item.id : "board"}
data-wide={
(item.type === "builtin" && item.id === "search" && searchDisplay === "input") || undefined
(item.type === "builtin" &&
((item.id === "search" && searchDisplay === "input") ||
(item.id === "logo" && logoDisplay === "logoAndText"))) ||
undefined
}
aria-label={configureLabel}
onClick={(event) => {
@@ -458,6 +484,8 @@ const SortableHeaderItem = ({
label={label}
compact={false}
searchDisplay={searchDisplay}
logoDisplay={logoDisplay}
logoTitle={logoTitle}
boardLogo={boardLogo}
/>
</button>
@@ -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 = (
<PreviewItem item={item} label={label} compact={compact} searchDisplay={searchDisplay} boardLogo={boardLogo} />
<PreviewItem
item={item}
label={label}
compact={compact}
searchDisplay={searchDisplay}
logoDisplay={logoDisplay}
logoTitle={logoTitle}
boardLogo={boardLogo}
/>
);
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 <HomarrLogo size={26} />;
if (item.id === "logo") {
return (
<div className={classes.logoPreview} data-with-title={logoDisplay === "logoAndText" || undefined}>
<HomarrLogo size={26} />
{logoDisplay === "logoAndText" ? <span>{logoTitle}</span> : null}
</div>
);
}
if (item.id === "search") {
const showInput = searchDisplay === "input" && !compact;
+4 -1
View File
@@ -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 (
<ConfigurableHeader
logo={logo ?? <HomarrLogo size={appShellLogoHeight} />}
logoWithTitle={logoWithTitle ?? <HomarrLogoWithTitle size="md" />}
actions={actions}
boardEditAction={boardEditAction}
boardSettingsAction={boardSettingsAction}
@@ -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;
@@ -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<typeof useOptionalHomarrAssistant>;
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 (
<UnstyledButton component={Link} href="/" className={classes.logo} aria-label={label("logo")}>
{logo}
</UnstyledButton>
<HeaderLogo
display={logoDisplay}
logo={logo}
logoWithTitle={logoWithTitle}
label={label("logo")}
/>
);
}
@@ -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;
}
@@ -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 (
<UnstyledButton
component={Link}
href="/"
className={classes.root}
data-display={display}
aria-label={label}
>
{content}
</UnstyledButton>
);
};
@@ -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,
};
@@ -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,
},
};
};
@@ -9,14 +9,18 @@ interface LogoProps {
shouldUseNextImage?: boolean;
}
export const Logo = ({ size = 60, shouldUseNextImage = false, src, alt }: LogoProps) =>
shouldUseNextImage ? (
<Image className="logo" src={src} alt={alt} width={size} height={size} />
export const Logo = ({ size = 60, shouldUseNextImage = false, src, alt }: LogoProps) => {
const normalizedSrc = src.trim();
if (normalizedSrc.length === 0) return null;
return shouldUseNextImage ? (
<Image className="logo" src={normalizedSrc} alt={alt} width={size} height={size} />
) : (
// 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
<img className="logo" src={src} alt={alt} width={size} height={size} />
<img className="logo" src={normalizedSrc} alt={alt} width={size} height={size} />
);
};
const logoWithTitleSizes = {
lg: { logoSize: 48, titleOrder: 1 },
+1 -1
View File
@@ -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 }))
@@ -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"}}');
@@ -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",
@@ -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"}}')
@@ -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",
@@ -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"}}')
@@ -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",
+4
View File
@@ -5725,6 +5725,10 @@
"searchDisplay": {
"input": "Search bar",
"icon": "Icon only"
},
"logoDisplay": {
"logo": "Logo only",
"logoAndText": "Logo + text"
}
},
"items": {
@@ -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")],
+2
View File
@@ -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,