diff --git a/apps/dokploy/__test__/traefik/server/update-server-config.test.ts b/apps/dokploy/__test__/traefik/server/update-server-config.test.ts index d1a03fedc..f82fdeb6b 100644 --- a/apps/dokploy/__test__/traefik/server/update-server-config.test.ts +++ b/apps/dokploy/__test__/traefik/server/update-server-config.test.ts @@ -62,6 +62,10 @@ const baseSettings: WebServerSettings = { errorPageDescription: null, ogImageUrl: null, footerText: null, + logoLightUrl: null, + loginLogoLightUrl: null, + faviconLightUrl: null, + dualModeBranding: false, }, cleanupCacheApplications: false, cleanupCacheOnCompose: false, diff --git a/apps/dokploy/components/dashboard/impersonation/impersonation-bar.tsx b/apps/dokploy/components/dashboard/impersonation/impersonation-bar.tsx index 5ab262ec8..8a66b5270 100644 --- a/apps/dokploy/components/dashboard/impersonation/impersonation-bar.tsx +++ b/apps/dokploy/components/dashboard/impersonation/impersonation-bar.tsx @@ -186,6 +186,7 @@ export const ImpersonationBar = () => { {!isImpersonating ? (
diff --git a/apps/dokploy/components/layouts/onboarding-layout.tsx b/apps/dokploy/components/layouts/onboarding-layout.tsx index ed46c1930..3776c2db5 100644 --- a/apps/dokploy/components/layouts/onboarding-layout.tsx +++ b/apps/dokploy/components/layouts/onboarding-layout.tsx @@ -19,6 +19,10 @@ export const OnboardingLayout = ({ children, leftPanel }: Props) => { "\u201CThe Open Source alternative to Netlify, Vercel, Heroku.\u201D"; const logoUrl = whitelabeling?.loginLogoUrl || whitelabeling?.logoUrl || undefined; + const logoLightUrl = + whitelabeling?.loginLogoLightUrl || + whitelabeling?.logoLightUrl || + undefined; return (
@@ -28,7 +32,11 @@ export const OnboardingLayout = ({ children, leftPanel }: Props) => { href="/" className="relative z-20 flex items-center text-lg font-medium gap-4 text-primary" > - + {appName}
diff --git a/apps/dokploy/components/proprietary/whitelabeling/whitelabeling-preview.tsx b/apps/dokploy/components/proprietary/whitelabeling/whitelabeling-preview.tsx index d53af90a2..5d1beea37 100644 --- a/apps/dokploy/components/proprietary/whitelabeling/whitelabeling-preview.tsx +++ b/apps/dokploy/components/proprietary/whitelabeling/whitelabeling-preview.tsx @@ -1,5 +1,6 @@ "use client"; +import { useTheme } from "next-themes"; import { Card, CardContent, @@ -12,12 +13,19 @@ interface WhitelabelingPreviewProps { config: { appName?: string; logoUrl?: string; + logoLightUrl?: string; + dualModeBranding?: boolean; footerText?: string; }; } export function WhitelabelingPreview({ config }: WhitelabelingPreviewProps) { const appName = config.appName || "Dokploy"; + const { resolvedTheme } = useTheme(); + const effectiveLogoUrl = + config.dualModeBranding && resolvedTheme === "light" && config.logoLightUrl + ? config.logoLightUrl + : config.logoUrl; return ( @@ -31,9 +39,9 @@ export function WhitelabelingPreview({ config }: WhitelabelingPreviewProps) {
{/* Simulated sidebar header */}
- {config.logoUrl ? ( + {effectiveLogoUrl ? ( Preview Logo diff --git a/apps/dokploy/components/proprietary/whitelabeling/whitelabeling-settings.tsx b/apps/dokploy/components/proprietary/whitelabeling/whitelabeling-settings.tsx index 3ba9e80bd..ee0e54e78 100644 --- a/apps/dokploy/components/proprietary/whitelabeling/whitelabeling-settings.tsx +++ b/apps/dokploy/components/proprietary/whitelabeling/whitelabeling-settings.tsx @@ -26,6 +26,7 @@ import { FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; +import { Switch } from "@/components/ui/switch"; import { Textarea } from "@/components/ui/textarea"; import { api } from "@/utils/api"; import { WhitelabelingPreview } from "./whitelabeling-preview"; @@ -36,20 +37,42 @@ const safeUrlField = z message: "Only http:// and https:// URLs are allowed", }); -const formSchema = z.object({ - appName: z.string(), - appDescription: z.string(), - logoUrl: safeUrlField, - faviconUrl: safeUrlField, - customCss: z.string(), - loginLogoUrl: safeUrlField, - supportUrl: safeUrlField, - docsUrl: safeUrlField, - errorPageTitle: z.string(), - errorPageDescription: z.string(), - footerText: z.string(), - ogImageUrl: safeUrlField, -}); +const formSchema = z + .object({ + appName: z.string(), + appDescription: z.string(), + logoUrl: safeUrlField, + faviconUrl: safeUrlField, + customCss: z.string(), + loginLogoUrl: safeUrlField, + supportUrl: safeUrlField, + docsUrl: safeUrlField, + errorPageTitle: z.string(), + errorPageDescription: z.string(), + footerText: z.string(), + ogImageUrl: safeUrlField, + logoLightUrl: z.string(), + loginLogoLightUrl: z.string(), + faviconLightUrl: z.string(), + dualModeBranding: z.boolean(), + }) + .superRefine((data, ctx) => { + if (!data.dualModeBranding) return; + for (const field of [ + "logoLightUrl", + "loginLogoLightUrl", + "faviconLightUrl", + ] as const) { + const val = data[field]; + if (val !== "" && !/^https?:\/\//i.test(val)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Only http:// and https:// URLs are allowed", + path: [field], + }); + } + } + }); type FormSchema = z.infer; @@ -195,6 +218,10 @@ export function WhitelabelingSettings() { errorPageDescription: "", footerText: "", ogImageUrl: "", + logoLightUrl: "", + loginLogoLightUrl: "", + faviconLightUrl: "", + dualModeBranding: false, }, resolver: zodResolver(formSchema), }); @@ -214,6 +241,10 @@ export function WhitelabelingSettings() { errorPageDescription: data.errorPageDescription ?? "", footerText: data.footerText ?? "", ogImageUrl: data.ogImageUrl ?? "", + logoLightUrl: data.logoLightUrl ?? "", + loginLogoLightUrl: data.loginLogoLightUrl ?? "", + faviconLightUrl: data.faviconLightUrl ?? "", + dualModeBranding: data.dualModeBranding ?? false, }); } }, [data, form]); @@ -244,6 +275,10 @@ export function WhitelabelingSettings() { errorPageDescription: values.errorPageDescription || null, footerText: values.footerText || null, ogImageUrl: values.ogImageUrl || null, + logoLightUrl: values.logoLightUrl || null, + loginLogoLightUrl: values.loginLogoLightUrl || null, + faviconLightUrl: values.faviconLightUrl || null, + dualModeBranding: values.dualModeBranding, }, }) .then(async () => { @@ -412,6 +447,111 @@ export function WhitelabelingSettings() { + {/* Light Mode Logos Section */} + + +
+
+ Light Mode Logos + + Use separate logos for light and dark themes. When enabled, + the logos in Branding are used for dark mode. + +
+ ( + + + { + field.onChange(checked); + if (!checked) { + form.clearErrors([ + "logoLightUrl", + "loginLogoLightUrl", + "faviconLightUrl", + ]); + } + }} + /> + + + )} + /> +
+
+
+
+ + ( + + Logo URL + + + + + + )} + /> + + ( + + Login Page Logo URL + + + + + + )} + /> + + ( + + Favicon URL + + + + + Follows the browser/OS theme, not the website theme. + + + + )} + /> + +
+
+
+ {/* Appearance Section */} diff --git a/apps/dokploy/components/shared/logo.tsx b/apps/dokploy/components/shared/logo.tsx index 2c316e22f..500925df3 100644 --- a/apps/dokploy/components/shared/logo.tsx +++ b/apps/dokploy/components/shared/logo.tsx @@ -1,16 +1,25 @@ +import { useTheme } from "next-themes"; import { cn } from "@/lib/utils"; interface Props { className?: string; logoUrl?: string; + logoLightUrl?: string; } -export const Logo = ({ className = "size-14", logoUrl }: Props) => { - if (logoUrl) { +export const Logo = ({ + className = "size-14", + logoUrl, + logoLightUrl, +}: Props) => { + const { resolvedTheme } = useTheme(); + const effectiveUrl = + resolvedTheme === "light" && logoLightUrl ? logoLightUrl : logoUrl; + + if (effectiveUrl) { return ( - // biome-ignore lint/performance/noImgElement: this is for dynamic logo loading Organization Logo diff --git a/apps/dokploy/pages/_document.tsx b/apps/dokploy/pages/_document.tsx index a8f3b5931..5e6b6948b 100644 --- a/apps/dokploy/pages/_document.tsx +++ b/apps/dokploy/pages/_document.tsx @@ -13,6 +13,7 @@ interface WhitelabelingDocumentProps { appDescription: string | null; ogImageUrl: string | null; faviconHref: string | null; + faviconLightHref: string | null; customCss: string | null; baseUrl: string; } @@ -22,6 +23,7 @@ export default function Document({ appDescription, ogImageUrl, faviconHref, + faviconLightHref, customCss, baseUrl, }: WhitelabelingDocumentProps) { @@ -44,7 +46,22 @@ export default function Document({ - + {faviconLightHref ? ( + <> + + + + ) : ( + + )} {customCss && (