preserve colorful brand logos

This commit is contained in:
134 2026-07-08 15:22:17 +08:00
parent 599abc5021
commit 2bbdd0aa61
14 changed files with 27 additions and 208 deletions

View File

@ -501,11 +501,6 @@ export default function Page() {
const displayLogo = mounted
? (resolvedTheme === "dark" && siteLogoDark ? siteLogoDark : siteLogo)
: siteLogo;
const shouldInvertLogo =
mounted &&
resolvedTheme === "dark" &&
!siteLogoDark?.trim() &&
Boolean(siteLogo?.trim());
const hasThirdPartyLogin =
(isDingTalkEnabled && (Boolean(dingTalkAppKey) || dingTalkOrganizations.length > 0)) ||
(isGoogleEnabled && isGoogleConfigured);
@ -545,7 +540,7 @@ export default function Page() {
<img
src={displayLogo}
alt={siteName}
className={`mb-2 h-12 w-auto object-contain ${shouldInvertLogo ? "brightness-0 invert" : ""}`}
className="mb-2 h-12 w-auto object-contain"
/>
) : null}
<h3 className="font-semibold text-xl dark:text-zinc-50">登录 {siteName}</h3>

View File

@ -8,7 +8,6 @@ export const dynamic = "force-dynamic";
export const revalidate = 0;
const DEFAULT_CONTENT_TYPE = "image/x-icon";
const DARK_FAVICON_CONTENT_TYPE = "image/png";
const FETCH_TIMEOUT_MS = 10_000;
function noStoreHeaders(contentType: string) {
@ -71,86 +70,16 @@ function uniqueNonEmptyValues(values: string[]) {
return result;
}
async function readLocalFallbackFavicon(preferDark = false) {
async function readLocalFallbackFavicon() {
const file = await readFile(
join(process.cwd(), "public", "default-favicon.ico")
);
if (preferDark) {
const transformed = await transformFaviconForDark(file).catch(() => null);
const fallback =
transformed || (await renderDarkFallbackFavicon().catch(() => null));
if (fallback) {
return new NextResponse(fallback, {
status: 200,
headers: noStoreHeaders(DARK_FAVICON_CONTENT_TYPE),
});
}
}
return new NextResponse(file, {
status: 200,
headers: noStoreHeaders(DEFAULT_CONTENT_TYPE),
});
}
async function renderDarkFallbackFavicon() {
const sharp = (await import("sharp")).default;
const svg = `\
<svg width="64" height="64" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
<rect width="64" height="64" fill="none"/>
<path d="M15 9h34v12H29v13h20v12H29v9l-14-11V9z" fill="#fff"/>
<path d="M37 25h12v30L37 45V25z" fill="#fff" opacity=".92"/>
</svg>`;
return await sharp(Buffer.from(svg)).png().toBuffer();
}
async function transformFaviconForDark(buffer: Buffer | ArrayBuffer) {
const sharp = (await import("sharp")).default;
const inputBuffer = Buffer.isBuffer(buffer)
? buffer
: Buffer.from(new Uint8Array(buffer));
const createBaseImage = () =>
sharp(inputBuffer, {
animated: false,
failOn: "none",
}).resize({
width: 64,
height: 64,
fit: "contain",
background: { r: 0, g: 0, b: 0, alpha: 0 },
});
const { data, info } = await createBaseImage()
.ensureAlpha()
.raw()
.toBuffer({ resolveWithObject: true });
let visiblePixelCount = 0;
let luminanceTotal = 0;
for (let index = 0; index < data.length; index += info.channels) {
const alpha = data[index + 3] ?? 255;
if (alpha < 24) continue;
const red = data[index] ?? 0;
const green = data[index + 1] ?? 0;
const blue = data[index + 2] ?? 0;
luminanceTotal += 0.2126 * red + 0.7152 * green + 0.0722 * blue;
visiblePixelCount += 1;
}
const averageLuminance =
visiblePixelCount > 0 ? luminanceTotal / visiblePixelCount : 0;
const needsInvertForDark = averageLuminance < 150;
const output = createBaseImage();
if (needsInvertForDark) {
output.negate({ alpha: false });
}
return await output
.png()
.toBuffer();
}
export async function GET(request: Request) {
try {
const requestUrl = new URL(request.url);
@ -159,20 +88,14 @@ export async function GET(request: Request) {
const configuredFavicon = String(settings?.favicon || "").trim();
const configuredLightLogo = String(settings?.siteLogo || "").trim();
const configuredDarkLogo = String(settings?.siteLogoDark || "").trim();
const rawFaviconCandidates = preferDark
? uniqueNonEmptyValues([
configuredFavicon,
configuredDarkLogo,
configuredLightLogo,
])
: uniqueNonEmptyValues([
configuredFavicon,
configuredLightLogo,
configuredDarkLogo,
]);
const rawFaviconCandidates = configuredFavicon
? [configuredFavicon]
: preferDark
? uniqueNonEmptyValues([configuredDarkLogo, configuredLightLogo])
: uniqueNonEmptyValues([configuredLightLogo, configuredDarkLogo]);
if (rawFaviconCandidates.length === 0) {
return await readLocalFallbackFavicon(preferDark);
return await readLocalFallbackFavicon();
}
for (const rawFaviconUrl of rawFaviconCandidates) {
@ -220,42 +143,15 @@ export async function GET(request: Request) {
const contentType =
upstream.headers.get("content-type") || inferContentType(faviconUrl);
if (preferDark) {
const transformed = await transformFaviconForDark(buffer).catch((error) => {
console.error("[favicon] Failed to transform dark favicon candidate:", {
targetUrl,
error,
});
return null;
});
if (!transformed) continue;
return new NextResponse(transformed, {
status: 200,
headers: noStoreHeaders(DARK_FAVICON_CONTENT_TYPE),
});
}
return new NextResponse(buffer, {
status: 200,
headers: noStoreHeaders(contentType),
});
}
if (preferDark) {
const fallback = await renderDarkFallbackFavicon().catch(() => null);
if (fallback) {
return new NextResponse(fallback, {
status: 200,
headers: noStoreHeaders(DARK_FAVICON_CONTENT_TYPE),
});
}
}
return await readLocalFallbackFavicon(preferDark);
return await readLocalFallbackFavicon();
} catch (error) {
console.error("[favicon] Failed to proxy favicon:", error);
const preferDark = new URL(request.url).searchParams.get("theme") === "dark";
return await readLocalFallbackFavicon(preferDark);
return await readLocalFallbackFavicon();
}
}

View File

@ -9640,10 +9640,6 @@ export default function CanvasEditorLegacyPage({
: lightCanvasBrandLogo || darkCanvasBrandLogo
: lightCanvasBrandLogo || darkCanvasBrandLogo
).trim();
const shouldInvertCanvasBrandLogo =
isDarkCanvasTheme &&
Boolean(canvasBrandLogo) &&
!hasSeparateDarkCanvasBrandLogo;
const brandEntry = (
<div
@ -9658,11 +9654,7 @@ export default function CanvasEditorLegacyPage({
{canvasBrandLogo ? (
<img
alt={siteSettingsData?.siteName || "返回首页"}
className={cn(
"h-7 w-7 object-contain",
shouldInvertCanvasBrandLogo && "brightness-0 invert",
!hasSeparateDarkCanvasBrandLogo && "dark:brightness-0 dark:invert"
)}
className="h-7 w-7 object-contain"
draggable={false}
src={canvasBrandLogo}
/>

View File

@ -24,19 +24,10 @@ export async function generateMetadata(): Promise<Metadata> {
"AI design workspace for Canvas, Agent workflows, image editing, 3D/CAD, and local AI operations.",
icons: {
icon: [
{
url: "/api/favicon?theme=dark",
media: "(prefers-color-scheme: dark)",
sizes: "any",
type: "image/png",
},
{
url: "/api/favicon",
media: "(prefers-color-scheme: light)",
sizes: "any",
},
{ url: "/api/favicon?theme=dark", sizes: "any", type: "image/png" },
{ url: "/api/favicon", type: "image/x-icon" },
],
apple: "/api/favicon",
shortcut: "/api/favicon",

View File

@ -53,7 +53,6 @@ export function AgentCanvasTopNavDark({
}: AgentCanvasTopNavDarkProps) {
const resolvedSiteName = String(siteName || "AI Chatbot").trim() || "AI Chatbot";
const displayLogo = siteLogoDark?.trim() || siteLogo?.trim() || "";
const shouldInvertLogo = !siteLogoDark?.trim() && Boolean(siteLogo?.trim());
return (
<header className="fixed top-0 left-0 right-0 z-50 border-b border-white/5 bg-black/80 backdrop-blur-xl">
@ -80,10 +79,7 @@ export function AgentCanvasTopNavDark({
<img
src={displayLogo}
alt={resolvedSiteName}
className={cn(
"h-8 w-8 shrink-0 object-contain",
shouldInvertLogo && "brightness-0 invert"
)}
className="h-8 w-8 shrink-0 object-contain"
/>
) : (
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-white/10">

View File

@ -6695,17 +6695,13 @@ function StylizedBrandLogo({
}) {
const resolvedSiteName = resolveSiteName(siteName);
const displayLogo = siteLogoDark?.trim() || siteLogo?.trim() || "";
const shouldInvertLogo = !siteLogoDark?.trim() && Boolean(siteLogo?.trim());
return (
<span className="flex min-w-0 items-center gap-2.5">
{displayLogo ? (
<img
alt={resolvedSiteName}
className={cn(
"h-8 w-8 shrink-0 object-contain",
shouldInvertLogo && "brightness-0 invert"
)}
className="h-8 w-8 shrink-0 object-contain"
src={displayLogo}
/>
) : (

View File

@ -33,9 +33,7 @@ export function CanvasLogoLoadingView({
alt={resolvedSiteName}
className={cn(
"h-[34px] w-[34px] object-contain",
hasSeparateDarkLogo
? "dark:hidden"
: "dark:brightness-0 dark:invert"
hasSeparateDarkLogo && "dark:hidden"
)}
src={lightLogo}
/>

View File

@ -5,24 +5,12 @@ import { useEffect } from "react";
export function FaviconUpdater() {
useEffect(() => {
let isDisposed = false;
const mediaQuery = window.matchMedia?.("(prefers-color-scheme: dark)");
const getFaviconUrl = () => {
const html = document.documentElement;
const hasDarkClass = html.classList.contains("dark");
const hasLightClass = html.classList.contains("light");
const prefersDark = Boolean(mediaQuery?.matches);
const isDark = hasDarkClass || (!hasLightClass && prefersDark);
return isDark ? "/api/favicon?theme=dark" : "/api/favicon";
};
const getFaviconUrl = () => "/api/favicon";
const updateFavicon = () => {
if (isDisposed) return;
const urlWithTheme = getFaviconUrl();
const iconType = urlWithTheme.includes("theme=dark")
? "image/png"
: "image/x-icon";
const href =
`${urlWithTheme}${urlWithTheme.includes("?") ? "&" : "?"}v=${Date.now()}`;
const managedSelector =
@ -35,12 +23,12 @@ export function FaviconUpdater() {
link.href = href;
link.setAttribute("data-managed-favicon", "true");
if (link.rel === "icon" || link.rel === "shortcut icon") {
link.type = iconType;
link.removeAttribute("type");
link.setAttribute("sizes", "any");
}
});
const ensureLink = (rel: string, type?: string) => {
const ensureLink = (rel: string) => {
let link = document.querySelector(
`link[rel="${rel}"]`
) as HTMLLinkElement | null;
@ -51,17 +39,15 @@ export function FaviconUpdater() {
}
link.href = href;
link.setAttribute("data-managed-favicon", "true");
if (type) {
link.type = type;
}
link.removeAttribute("type");
if (rel === "icon") {
link.setAttribute("sizes", "any");
}
};
ensureLink("icon", iconType);
ensureLink("icon");
ensureLink("apple-touch-icon");
ensureLink("shortcut icon", iconType);
ensureLink("shortcut icon");
};
const observer = new MutationObserver(updateFavicon);
@ -69,8 +55,6 @@ export function FaviconUpdater() {
attributes: true,
attributeFilter: ["class"],
});
mediaQuery?.addEventListener?.("change", updateFavicon);
Promise.resolve().then(() => {
updateFavicon();
});
@ -78,7 +62,6 @@ export function FaviconUpdater() {
return () => {
isDisposed = true;
observer.disconnect();
mediaQuery?.removeEventListener?.("change", updateFavicon);
};
}, []);

View File

@ -677,8 +677,6 @@ export function LovartHome({
resolvedTheme === "dark"
? siteLogoDark || siteLogo || ""
: siteLogo || siteLogoDark || "";
const shouldInvertLogo =
resolvedTheme === "dark" && !siteLogoDark && Boolean(siteLogo);
const { data: tutorialVideosData } = useSWR<{ videos: TutorialVideo[] }>(
isSecondaryDataReady && siteSettingsData?.tutorialEnabled
@ -1564,7 +1562,6 @@ export function LovartHome({
contentClassName="pl-[98px]"
logo={displayLogo}
onLogoClick={() => window.scrollTo({ top: 0, behavior: "smooth" })}
shouldInvertLogo={shouldInvertLogo}
showBrand={false}
siteName={siteName || "Lovart"}
>
@ -1814,7 +1811,6 @@ export function LovartHome({
? "/settings/agent-workflow"
: null
}
shouldInvertLogo={shouldInvertLogo}
siteLogo={displayLogo}
siteName={siteName || "Lovart"}
toolbox1Enabled={siteSettingsData?.toolbox1Enabled !== false}
@ -1852,7 +1848,6 @@ export function LovartHome({
prompt={prompt}
removeFile={removeFile}
setPrompt={setPrompt}
shouldInvertLogo={shouldInvertLogo}
siteName={siteName}
selectedModelId={selectedComposerModel.id}
/>
@ -1990,7 +1985,7 @@ export function LovartHome({
<img
src={displayLogo}
alt={siteName}
className={`h-7 w-7 rounded-full object-cover ${shouldInvertLogo ? "brightness-0 invert" : ""}`}
className="h-7 w-7 rounded-full object-cover"
/>
) : (
<div className="flex h-7 w-7 items-center justify-center rounded-full bg-black text-white dark:bg-white dark:text-black">

View File

@ -47,7 +47,6 @@ type LovartHeroComposerProps = {
prompt: string;
removeFile: (id: string) => void;
setPrompt: (value: string) => void;
shouldInvertLogo: boolean;
siteName: string;
modelOptions: ModelOption[];
onModelChange: (modelId: string) => void;
@ -73,7 +72,6 @@ export function LovartHeroComposer({
prompt,
removeFile,
setPrompt,
shouldInvertLogo,
siteName,
modelOptions,
onModelChange,
@ -104,7 +102,7 @@ export function LovartHeroComposer({
<img
src={displayLogo}
alt={siteName}
className={`h-10 w-10 rounded-full object-cover ${shouldInvertLogo ? "brightness-0 invert" : ""}`}
className="h-10 w-10 rounded-full object-cover"
/>
) : (
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-black text-white dark:bg-white dark:text-black">

View File

@ -42,7 +42,6 @@ type LovartSidebarProps = {
}
) => ReactNode;
settingsHref: string | null;
shouldInvertLogo?: boolean;
siteLogo?: string | null;
siteName: string;
toolbox1Enabled: boolean;
@ -71,7 +70,6 @@ export function LovartSidebar({
productPlanningEnabled,
renderIcon,
settingsHref,
shouldInvertLogo = false,
siteLogo,
siteName,
toolbox1Enabled,
@ -184,12 +182,7 @@ export function LovartSidebar({
<img
src={siteLogo}
alt={siteName}
className={[
"h-8 w-8 rounded-full object-cover",
shouldInvertLogo ? "brightness-0 invert" : "",
]
.filter(Boolean)
.join(" ")}
className="h-8 w-8 rounded-full object-cover"
/>
) : (
<span className="flex h-8 w-8 items-center justify-center rounded-full bg-black text-[13px] font-semibold text-white dark:bg-white dark:text-black">

View File

@ -12,7 +12,6 @@ type BrandTopBarProps = {
logo?: string | null;
onLogoClick?: () => void;
position?: "fixed" | "sticky" | "static";
shouldInvertLogo?: boolean;
showBrand?: boolean;
siteName: string;
};
@ -25,7 +24,6 @@ export function BrandTopBar({
logo,
onLogoClick,
position = "fixed",
shouldInvertLogo = false,
showBrand = true,
siteName,
}: BrandTopBarProps) {
@ -61,10 +59,7 @@ export function BrandTopBar({
{logo ? (
<img
alt={resolvedSiteName}
className={cn(
"h-8 w-8 rounded-full object-cover",
shouldInvertLogo && "brightness-0 invert"
)}
className="h-8 w-8 rounded-full object-cover"
src={logo}
/>
) : (

View File

@ -59,8 +59,6 @@ export function PascalEditorFrame({
const lightLogo = String(siteLogo || "").trim();
return darkLogo || lightLogo;
}, [siteLogo, siteLogoDark]);
const shouldInvertLogo =
!String(siteLogoDark || "").trim() && Boolean(String(siteLogo || "").trim());
const userEmail = String(user?.email || "").trim();
const userLabel = String(user?.name || userEmail || "已登录用户").trim();
const isGuest = guestRegex.test(userEmail);
@ -91,10 +89,7 @@ export function PascalEditorFrame({
{displayLogo ? (
<img
alt={resolvedSiteName}
className={cn(
"h-7 w-7 shrink-0 object-contain",
shouldInvertLogo && "brightness-0 invert"
)}
className="h-7 w-7 shrink-0 object-contain"
src={displayLogo}
/>
) : (

View File

@ -150,7 +150,7 @@ export function TopNavbar({
}
};
// 决定显示哪个 LOGO(浅色用普通,深色用暗色)
// 彩色 logo 不能用滤镜自动反白;暗色主题只在配置了独立 dark logo 时切换。
const displayLogo =
resolvedTheme === "dark"
? siteLogoDark && siteLogoDark.trim() !== ""
@ -159,10 +159,6 @@ export function TopNavbar({
: siteLogo && siteLogo.trim() !== ""
? siteLogo
: siteLogoDark;
const shouldInvertLogo =
resolvedTheme === "dark" &&
(!siteLogoDark || siteLogoDark.trim() === "") &&
Boolean(siteLogo && siteLogo.trim() !== "");
const getShortLabel = (label: string) => {
const trimmed = label.trim();
@ -287,7 +283,7 @@ export function TopNavbar({
{displayLogo && (
<img
alt={siteName}
className={shouldInvertLogo ? "h-7 w-7 object-contain brightness-0 invert" : "h-7 w-7 object-contain"}
className="h-7 w-7 object-contain"
src={displayLogo}
/>
)}