From e8a950b0b6639375fde19085bfc8ee95f26f2352 Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Mon, 22 Jun 2026 09:20:08 +0330 Subject: [PATCH] background image --- .../[name]/(Main)/about/hooks/useAboutData.ts | 19 ++- src/app/[name]/(Main)/hooks/useMenuData.ts | 4 +- src/app/globals.css | 17 ++ src/components/background/AppBackground.tsx | 57 ++++++- .../background/PatternBackgroundProvider.tsx | 57 +++++-- src/components/wrapper/PreferenceWrapper.tsx | 9 +- src/lib/helpers/backgroundUtils.ts | 21 ++- src/lib/helpers/themeBootScript.ts | 2 +- src/lib/helpers/themeCache.ts | 155 ++++++++++++++---- src/styles/glass.css | 6 +- 10 files changed, 286 insertions(+), 61 deletions(-) diff --git a/src/app/[name]/(Main)/about/hooks/useAboutData.ts b/src/app/[name]/(Main)/about/hooks/useAboutData.ts index 292c24a..997143d 100644 --- a/src/app/[name]/(Main)/about/hooks/useAboutData.ts +++ b/src/app/[name]/(Main)/about/hooks/useAboutData.ts @@ -1,11 +1,15 @@ import { useQuery } from "@tanstack/react-query"; import { useParams } from "next/navigation"; +import { useEffect } from "react"; import * as api from "../service/AboutService"; -import { setPersistedAboutData } from "@/lib/helpers/themeCache"; +import { + getPersistedAboutData, + setPersistedAboutData, +} from "@/lib/helpers/themeCache"; export const useGetAbout = () => { const { name } = useParams<{ name: string }>(); - return useQuery({ + const query = useQuery({ queryKey: ["about", name], queryFn: async () => { const data = await api.getAbout(name); @@ -16,9 +20,18 @@ export const useGetAbout = () => { retry: false, staleTime: 60 * 60_000, gcTime: 24 * 60 * 60_000, - refetchOnMount: false, + refetchOnMount: "always", refetchOnWindowFocus: false, + placeholderData: () => (name ? getPersistedAboutData(name) : undefined), }); + + useEffect(() => { + if (name && query.data) { + setPersistedAboutData(name, query.data); + } + }, [name, query.data]); + + return query; }; export const useGetReviews = () => { diff --git a/src/app/[name]/(Main)/hooks/useMenuData.ts b/src/app/[name]/(Main)/hooks/useMenuData.ts index ebe42af..9014294 100644 --- a/src/app/[name]/(Main)/hooks/useMenuData.ts +++ b/src/app/[name]/(Main)/hooks/useMenuData.ts @@ -23,7 +23,7 @@ export const useGetFoods = () => { }, enabled: !!name, staleTime: 5 * 60_000, - refetchOnMount: false, + refetchOnMount: "always", placeholderData: () => (name ? getPersistedMenuData(name) : undefined), }); @@ -47,7 +47,7 @@ export const useGetCategories = () => { }, enabled: !!name, staleTime: 5 * 60_000, - refetchOnMount: false, + refetchOnMount: "always", placeholderData: () => (name ? getPersistedCategoriesData(name) : undefined), }); diff --git a/src/app/globals.css b/src/app/globals.css index 750181f..09fc9bb 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -413,4 +413,21 @@ html[data-pattern-bg="true"] [data-slot="splash-screen"] { html[data-pattern-bg="true"] .pattern-secondary-bg { background-color: var(--primary-light) !important; +} + +html[data-image-bg="true"] #root { + position: relative; + z-index: 1; +} + +html[data-image-bg="true"] [data-slot="dialog-content"] { + background-color: var(--container) !important; +} + +html[data-image-bg="true"] [data-slot="splash-screen"] { + background-color: var(--container) !important; +} + +html[data-image-bg="true"] .pattern-secondary-bg { + background-color: var(--primary-light) !important; } \ No newline at end of file diff --git a/src/components/background/AppBackground.tsx b/src/components/background/AppBackground.tsx index bb4557e..eab0dda 100644 --- a/src/components/background/AppBackground.tsx +++ b/src/components/background/AppBackground.tsx @@ -4,11 +4,15 @@ import { usePatternBackground } from "@/components/background/PatternBackgroundP import { THEME_BG_ELEMENT_ID } from "@/lib/helpers/themeCache"; import { useLayoutEffect } from "react"; +const THEME_BG_IMG_ID = "cached-theme-bg-img"; +const THEME_BG_OVERLAY_ID = "cached-theme-bg-overlay"; + export default function AppBackground() { - const { isPattern, backgroundStyle } = usePatternBackground(); + const { isPattern, backgroundStyle, isCustomImage, customImageSettings } = + usePatternBackground(); useLayoutEffect(() => { - if (!isPattern) { + if (!isPattern && !isCustomImage) { document.getElementById(THEME_BG_ELEMENT_ID)?.remove(); return; } @@ -25,8 +29,53 @@ export default function AppBackground() { document.body.appendChild(element); } - Object.assign(element.style, backgroundStyle as Record); - }, [backgroundStyle, isPattern]); + if (isPattern) { + element.style.overflow = ""; + document.getElementById(THEME_BG_IMG_ID)?.remove(); + document.getElementById(THEME_BG_OVERLAY_ID)?.remove(); + Object.assign(element.style, backgroundStyle as Record); + return; + } + + if (isCustomImage && customImageSettings) { + const { bgUrl, bgBlur, bgOpacity, bgOverlay } = customImageSettings; + + element.style.backgroundColor = ""; + element.style.backgroundImage = ""; + element.style.overflow = "hidden"; + + let imgLayer = document.getElementById(THEME_BG_IMG_ID); + if (!imgLayer) { + imgLayer = document.createElement("div"); + imgLayer.id = THEME_BG_IMG_ID; + imgLayer.style.position = "absolute"; + imgLayer.style.inset = "-40px"; + imgLayer.style.backgroundSize = "cover"; + imgLayer.style.backgroundPosition = "center"; + imgLayer.style.backgroundRepeat = "no-repeat"; + element.appendChild(imgLayer); + } + + imgLayer.style.backgroundImage = `url("${bgUrl}")`; + imgLayer.style.filter = bgBlur ? `blur(${bgBlur}px)` : ""; + imgLayer.style.opacity = + bgOpacity != null ? String(bgOpacity / 100) : "1"; + + let overlayLayer = document.getElementById(THEME_BG_OVERLAY_ID); + if (!overlayLayer) { + overlayLayer = document.createElement("div"); + overlayLayer.id = THEME_BG_OVERLAY_ID; + overlayLayer.style.position = "absolute"; + overlayLayer.style.inset = "0"; + element.appendChild(overlayLayer); + } + + const overlayOpacity = + bgOverlay != null ? Number(bgOverlay) / 100 : 0; + overlayLayer.style.backgroundColor = + overlayOpacity > 0 ? `rgba(0,0,0,${overlayOpacity})` : ""; + } + }, [backgroundStyle, isPattern, isCustomImage, customImageSettings]); return null; } diff --git a/src/components/background/PatternBackgroundProvider.tsx b/src/components/background/PatternBackgroundProvider.tsx index c196453..26faa55 100644 --- a/src/components/background/PatternBackgroundProvider.tsx +++ b/src/components/background/PatternBackgroundProvider.tsx @@ -2,11 +2,15 @@ import { useGetAbout } from "@/app/[name]/(Main)/about/hooks/useAboutData"; import { + applyCustomImageCssVariables, applyPatternCssVariables, + clearCustomImageCssVariables, clearPatternCssVariables, colorizeSvg, hexToRgba, + isCustomImageBackground, isPatternBackground, + normalizeBgNumber, PATTERN_BACKGROUND_OPACITY, } from "@/lib/helpers/backgroundUtils"; import { @@ -16,6 +20,7 @@ import { removeCachedThemeBackground, setThemeCache, svgToDataUrl, + themeBackgroundMatches, type CachedRestaurantTheme, } from "@/lib/helpers/themeCache"; import { useParams } from "next/navigation"; @@ -32,14 +37,25 @@ import { const DEFAULT_MENU_COLOR = "#1E3A8A"; +export type CustomImageSettings = { + bgUrl: string; + bgBlur: number | null; + bgOpacity: number | null; + bgOverlay: string | null; +}; + type PatternBackgroundContextValue = { isPattern: boolean; + isCustomImage: boolean; backgroundStyle: CSSProperties; + customImageSettings: CustomImageSettings | null; }; const PatternBackgroundContext = createContext({ isPattern: false, + isCustomImage: false, backgroundStyle: {}, + customImageSettings: null, }); export function PatternBackgroundProvider({ children }: { children: ReactNode }) { @@ -52,6 +68,10 @@ export function PatternBackgroundProvider({ children }: { children: ReactNode }) const isPattern = restaurant ? isPatternBackground(restaurant) : (hydratedCache?.isPattern ?? false); + + const isCustomImage = restaurant + ? isCustomImageBackground(restaurant) + : (!hydratedCache?.isPattern && !!hydratedCache?.bgUrl); const menuColor = restaurant?.menuColor || hydratedCache?.menuColor || DEFAULT_MENU_COLOR; const baseTint = hexToRgba(menuColor, PATTERN_BACKGROUND_OPACITY); @@ -84,15 +104,21 @@ export function PatternBackgroundProvider({ children }: { children: ReactNode }) if (!themeBase.isPattern) { clearPatternCssVariables(); - removeCachedThemeBackground(); setPatternImageUrl(null); - setThemeCache(restaurantSlug, { - ...themeBase, - patternDataUrl: null, - }); + + if (isCustomImageBackground(restaurant)) { + applyCustomImageCssVariables(); + setThemeCache(restaurantSlug, { ...themeBase, patternDataUrl: null }); + } else { + clearCustomImageCssVariables(); + removeCachedThemeBackground(); + setThemeCache(restaurantSlug, { ...themeBase, patternDataUrl: null }); + } return; } + clearCustomImageCssVariables(); + applyPatternCssVariables(menuColor); if (!themeBase.bgUrl) { @@ -110,9 +136,9 @@ export function PatternBackgroundProvider({ children }: { children: ReactNode }) const cached = getThemeCache(restaurantSlug); const cacheHit = - cached?.isPattern && - cached.bgUrl === themeBase.bgUrl && - cached.menuColor === menuColor && + cached && + themeBackgroundMatches(cached, restaurant) && + cached.isPattern && cached.patternDataUrl; if (cacheHit) { @@ -178,9 +204,20 @@ export function PatternBackgroundProvider({ children }: { children: ReactNode }) [baseTint, patternImageUrl], ); + const customImageSettings = useMemo(() => { + const src = restaurant?.bgUrl || hydratedCache?.bgUrl; + if (!isCustomImage || !src) return null; + return { + bgUrl: src, + bgBlur: normalizeBgNumber(restaurant?.bgBlur ?? hydratedCache?.bgBlur), + bgOpacity: normalizeBgNumber(restaurant?.bgOpacity ?? hydratedCache?.bgOpacity), + bgOverlay: restaurant?.bgOverlay ?? hydratedCache?.bgOverlay ?? null, + }; + }, [isCustomImage, restaurant, hydratedCache]); + const value = useMemo( - () => ({ isPattern, backgroundStyle }), - [backgroundStyle, isPattern], + () => ({ isPattern, isCustomImage, backgroundStyle, customImageSettings }), + [backgroundStyle, isPattern, isCustomImage, customImageSettings], ); return ( diff --git a/src/components/wrapper/PreferenceWrapper.tsx b/src/components/wrapper/PreferenceWrapper.tsx index 3e0871d..8b3be6a 100644 --- a/src/components/wrapper/PreferenceWrapper.tsx +++ b/src/components/wrapper/PreferenceWrapper.tsx @@ -131,10 +131,14 @@ function PreferenceWrapper({ children }: Props) { const restaurant = aboutData.data; const cached = getThemeCache(restaurantSlug); + const themeBase = buildThemeCacheFromRestaurant(restaurant); setThemeCache(restaurantSlug, { menuColor, bgUrl: restaurant.bgUrl, - isPattern: buildThemeCacheFromRestaurant(restaurant).isPattern, + bgBlur: themeBase.bgBlur, + bgOpacity: themeBase.bgOpacity, + bgOverlay: themeBase.bgOverlay, + isPattern: themeBase.isPattern, patternDataUrl: cached?.patternDataUrl ?? null, }); @@ -155,8 +159,7 @@ function PreferenceWrapper({ children }: Props) { if (seenFingerprint === '|' && currentFingerprint !== '|') { localStorage.setItem(getSplashStorageKey(restaurantSlug), currentFingerprint); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [aboutData?.data?.menuColor, restaurantSlug]); + }, [aboutData?.data, restaurantSlug]); useEffect(() => { document.documentElement.setAttribute('data-theme', nightMode ? 'dark' : 'light'); diff --git a/src/lib/helpers/backgroundUtils.ts b/src/lib/helpers/backgroundUtils.ts index 88590f4..d09ae39 100644 --- a/src/lib/helpers/backgroundUtils.ts +++ b/src/lib/helpers/backgroundUtils.ts @@ -4,12 +4,19 @@ export const PRIMARY_LIGHT_OPACITY = 0.15; export type RestaurantBackgroundSettings = { bgUrl?: string | null; - bgBlur?: number | null; - bgOpacity?: number | null; + bgBlur?: number | string | null; + bgOpacity?: number | string | null; bgOverlay?: string | null; menuColor?: string | null; }; +export const normalizeBgNumber = (value: unknown): number | null => { + if (value == null || value === "") return null; + const parsed = typeof value === "string" ? Number(value) : value; + if (typeof parsed !== "number" || Number.isNaN(parsed)) return null; + return parsed; +}; + export const hexToRgba = (hex: string, alpha: number): string => { const normalized = hex.replace("#", ""); if (normalized.length !== 6) { @@ -82,3 +89,13 @@ export const clearPatternCssVariables = () => { root.style.removeProperty("--primary-light"); root.style.removeProperty("--background"); }; + +export const applyCustomImageCssVariables = () => { + const root = document.documentElement; + root.setAttribute("data-image-bg", "true"); + root.removeAttribute("data-pattern-bg"); +}; + +export const clearCustomImageCssVariables = () => { + document.documentElement.removeAttribute("data-image-bg"); +}; diff --git a/src/lib/helpers/themeBootScript.ts b/src/lib/helpers/themeBootScript.ts index d2e2230..b7c6198 100644 --- a/src/lib/helpers/themeBootScript.ts +++ b/src/lib/helpers/themeBootScript.ts @@ -1,3 +1,3 @@ export function getThemeBootScript(): string { - return `(function(){try{var s=location.pathname.split("/").filter(Boolean)[0],t=null;if(s&&s!=="auth"){var r=localStorage.getItem("restaurant-theme:"+s);if(r)t=JSON.parse(r)}if(!t||t.version!==1||!t.menuColor){var l=localStorage.getItem("theme-primary-color");if(l){var x=l.replace("#","");if(x.length===6){var p=parseInt(x.slice(0,2),16),f=parseInt(x.slice(2,4),16),m=parseInt(x.slice(4,6),16),b="rgba("+p+","+f+","+m+",0.05)";document.documentElement.style.setProperty("--background",b)}}return}function h(n,a){var u=n.replace("#","");if(u.length!==6)return null;var v=parseInt(u.slice(0,2),16),g=parseInt(u.slice(2,4),16),y=parseInt(u.slice(4,6),16);return"rgba("+v+","+g+","+y+","+a+")"}var e=document.documentElement;if(t.primaryOklch){e.style.setProperty("--primary",t.primaryOklch);e.style.setProperty("--primary-foreground",t.primaryForeground||"oklch(1 0 0)")}if(!t.isPattern)return;e.setAttribute("data-pattern-bg","true");var k=t.backgroundTint||h(t.menuColor,0.05),w=t.bgPatternVector||h(t.menuColor,0.1),z=t.primaryLight||h(t.menuColor,0.15);k&&(e.style.setProperty("--background",k),e.style.setProperty("--bg-pattern-base",k));w&&e.style.setProperty("--bg-pattern-vector",w);z&&e.style.setProperty("--primary-light",z);if(k){var d=document.createElement("div");d.id="cached-theme-bg";d.setAttribute("aria-hidden","true");d.style.cssText="position:fixed;inset:0;pointer-events:none;z-index:0;background-color:"+k+(t.patternDataUrl?';background-image:url("'+t.patternDataUrl+'");background-repeat:repeat;background-size:auto':"")+";";document.body.appendChild(d)}}catch(u){}})();`; + return `(function(){try{var s=location.pathname.split("/").filter(Boolean)[0],t=null;if(s&&s!=="auth"){var r=localStorage.getItem("restaurant-theme:"+s);if(r)t=JSON.parse(r)}if(!t||t.version!==1||!t.menuColor){var l=localStorage.getItem("theme-primary-color");if(l){var x=l.replace("#","");if(x.length===6){var p=parseInt(x.slice(0,2),16),f=parseInt(x.slice(2,4),16),m=parseInt(x.slice(4,6),16),b="rgba("+p+","+f+","+m+",0.05)";document.documentElement.style.setProperty("--background",b)}}return}function h(n,a){var u=n.replace("#","");if(u.length!==6)return null;var v=parseInt(u.slice(0,2),16),g=parseInt(u.slice(2,4),16),y=parseInt(u.slice(4,6),16);return"rgba("+v+","+g+","+y+","+a+")"}function isUnset(v){return v==null||v===""||v===0}function isCustomImg(th){if(!th.bgUrl)return false;return !isUnset(th.bgBlur)||!isUnset(th.bgOpacity)||!isUnset(th.bgOverlay)}var e=document.documentElement;if(t.primaryOklch){e.style.setProperty("--primary",t.primaryOklch);e.style.setProperty("--primary-foreground",t.primaryForeground||"oklch(1 0 0)")}if(!t.isPattern&&!isCustomImg(t))return;if(t.isPattern){e.setAttribute("data-pattern-bg","true");var k=t.backgroundTint||h(t.menuColor,0.05),w=t.bgPatternVector||h(t.menuColor,0.1),z=t.primaryLight||h(t.menuColor,0.15);k&&(e.style.setProperty("--background",k),e.style.setProperty("--bg-pattern-base",k));w&&e.style.setProperty("--bg-pattern-vector",w);z&&e.style.setProperty("--primary-light",z);if(k){var d=document.createElement("div");d.id="cached-theme-bg";d.setAttribute("aria-hidden","true");d.style.cssText="position:fixed;inset:0;pointer-events:none;z-index:0;background-color:"+k+(t.patternDataUrl?';background-image:url("'+t.patternDataUrl+'");background-repeat:repeat;background-size:auto':"")+";";document.body.appendChild(d)}}else if(isCustomImg(t)){e.setAttribute("data-image-bg","true");var d2=document.createElement("div");d2.id="cached-theme-bg";d2.setAttribute("aria-hidden","true");d2.style.cssText="position:fixed;inset:0;pointer-events:none;z-index:0;overflow:hidden;";var i=document.createElement("div");i.id="cached-theme-bg-img";i.style.cssText="position:absolute;inset:-40px;background-size:cover;background-position:center;background-repeat:no-repeat;background-image:url(\\""+t.bgUrl+"\\");"+(t.bgBlur?"filter:blur("+t.bgBlur+"px);":"")+"opacity:"+(t.bgOpacity!=null?t.bgOpacity/100:1)+";";d2.appendChild(i);var ov=document.createElement("div");ov.id="cached-theme-bg-overlay";ov.style.cssText="position:absolute;inset:0;"+(t.bgOverlay&&Number(t.bgOverlay)>0?"background-color:rgba(0,0,0,"+Number(t.bgOverlay)/100+");":"");d2.appendChild(ov);document.body.appendChild(d2)}}catch(u){}})();`; } diff --git a/src/lib/helpers/themeCache.ts b/src/lib/helpers/themeCache.ts index 2cec6a5..2da4992 100644 --- a/src/lib/helpers/themeCache.ts +++ b/src/lib/helpers/themeCache.ts @@ -1,7 +1,9 @@ import { calculateBrightness, hexToOklch } from "@/lib/helpers/colorUtils"; import { hexToRgba, + isCustomImageBackground, isPatternBackground, + normalizeBgNumber, PATTERN_BACKGROUND_OPACITY, PATTERN_VECTOR_OPACITY, PRIMARY_LIGHT_OPACITY, @@ -18,6 +20,9 @@ export type CachedRestaurantTheme = { version: 1; menuColor: string; bgUrl?: string | null; + bgBlur?: number | null; + bgOpacity?: number | null; + bgOverlay?: string | null; isPattern: boolean; patternDataUrl?: string | null; primaryOklch?: string; @@ -34,6 +39,8 @@ const ABOUT_CACHE_PREFIX = "restaurant-about"; const MENU_CACHE_PREFIX = "restaurant-menu"; const CATEGORIES_CACHE_PREFIX = "restaurant-categories"; export const THEME_BG_ELEMENT_ID = "cached-theme-bg"; +const THEME_BG_IMG_ID = "cached-theme-bg-img"; +const THEME_BG_OVERLAY_ID = "cached-theme-bg-overlay"; export function getThemeCacheKey(slug: string): string { return `${CACHE_KEY_PREFIX}:${slug}`; @@ -215,7 +222,10 @@ export function setThemeCache( const payload: CachedRestaurantTheme = { ...theme, ...css, - patternDataUrl: theme.patternDataUrl ?? existing?.patternDataUrl ?? null, + patternDataUrl: + theme.patternDataUrl !== undefined + ? theme.patternDataUrl + : (existing?.patternDataUrl ?? null), version: CACHE_VERSION, }; @@ -238,18 +248,54 @@ export function setThemeCache( export function buildThemeCacheFromRestaurant( settings: RestaurantBackgroundSettings, -): Pick { +): Pick< + CachedRestaurantTheme, + "menuColor" | "bgUrl" | "bgBlur" | "bgOpacity" | "bgOverlay" | "isPattern" +> { return { menuColor: settings.menuColor || "#1E3A8A", bgUrl: settings.bgUrl, + bgBlur: normalizeBgNumber(settings.bgBlur), + bgOpacity: normalizeBgNumber(settings.bgOpacity), + bgOverlay: settings.bgOverlay ?? null, isPattern: isPatternBackground(settings), }; } +export function themeBackgroundMatches( + cached: CachedRestaurantTheme, + settings: RestaurantBackgroundSettings, +): boolean { + const next = buildThemeCacheFromRestaurant(settings); + return ( + cached.menuColor === next.menuColor && + cached.bgUrl === next.bgUrl && + cached.bgBlur === next.bgBlur && + cached.bgOpacity === next.bgOpacity && + cached.bgOverlay === next.bgOverlay && + cached.isPattern === next.isPattern + ); +} + export function svgToDataUrl(svg: string): string { return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`; } +function ensureBgElement(): HTMLElement { + let bgElement = document.getElementById(THEME_BG_ELEMENT_ID); + if (!bgElement) { + bgElement = document.createElement("div"); + bgElement.id = THEME_BG_ELEMENT_ID; + bgElement.setAttribute("aria-hidden", "true"); + bgElement.style.position = "fixed"; + bgElement.style.inset = "0"; + bgElement.style.pointerEvents = "none"; + bgElement.style.zIndex = "0"; + document.body.appendChild(bgElement); + } + return bgElement; +} + export function applyCachedThemeToDom(theme: CachedRestaurantTheme): void { if (typeof document === "undefined") return; @@ -263,44 +309,85 @@ export function applyCachedThemeToDom(theme: CachedRestaurantTheme): void { ); } - if (!theme.isPattern) return; + const isCustomImage = isCustomImageBackground(theme); - root.setAttribute("data-pattern-bg", "true"); + if (!theme.isPattern && !isCustomImage) return; - if (theme.backgroundTint) { - root.style.setProperty("--background", theme.backgroundTint); - root.style.setProperty("--bg-pattern-base", theme.backgroundTint); - } - if (theme.bgPatternVector) { - root.style.setProperty("--bg-pattern-vector", theme.bgPatternVector); - } - if (theme.primaryLight) { - root.style.setProperty("--primary-light", theme.primaryLight); + if (theme.isPattern) { + root.setAttribute("data-pattern-bg", "true"); + root.removeAttribute("data-image-bg"); + + if (theme.backgroundTint) { + root.style.setProperty("--background", theme.backgroundTint); + root.style.setProperty("--bg-pattern-base", theme.backgroundTint); + } + if (theme.bgPatternVector) { + root.style.setProperty("--bg-pattern-vector", theme.bgPatternVector); + } + if (theme.primaryLight) { + root.style.setProperty("--primary-light", theme.primaryLight); + } + + if (!theme.backgroundTint) return; + + const bgElement = ensureBgElement(); + bgElement.style.overflow = ""; + + document.getElementById(THEME_BG_IMG_ID)?.remove(); + document.getElementById(THEME_BG_OVERLAY_ID)?.remove(); + + bgElement.style.backgroundColor = theme.backgroundTint; + if (theme.patternDataUrl) { + bgElement.style.backgroundImage = `url("${theme.patternDataUrl}")`; + bgElement.style.backgroundRepeat = "repeat"; + bgElement.style.backgroundSize = "auto"; + } else { + bgElement.style.removeProperty("background-image"); + bgElement.style.removeProperty("background-repeat"); + bgElement.style.removeProperty("background-size"); + } + return; } - if (!theme.backgroundTint) return; + if (isCustomImage && theme.bgUrl) { + root.setAttribute("data-image-bg", "true"); + root.removeAttribute("data-pattern-bg"); - let bgElement = document.getElementById(THEME_BG_ELEMENT_ID); - if (!bgElement) { - bgElement = document.createElement("div"); - bgElement.id = THEME_BG_ELEMENT_ID; - bgElement.setAttribute("aria-hidden", "true"); - bgElement.style.position = "fixed"; - bgElement.style.inset = "0"; - bgElement.style.pointerEvents = "none"; - bgElement.style.zIndex = "0"; - document.body.appendChild(bgElement); - } + const bgElement = ensureBgElement(); + bgElement.style.backgroundColor = ""; + bgElement.style.backgroundImage = ""; + bgElement.style.overflow = "hidden"; - bgElement.style.backgroundColor = theme.backgroundTint; - if (theme.patternDataUrl) { - bgElement.style.backgroundImage = `url("${theme.patternDataUrl}")`; - bgElement.style.backgroundRepeat = "repeat"; - bgElement.style.backgroundSize = "auto"; - } else { - bgElement.style.removeProperty("background-image"); - bgElement.style.removeProperty("background-repeat"); - bgElement.style.removeProperty("background-size"); + let imgLayer = document.getElementById(THEME_BG_IMG_ID); + if (!imgLayer) { + imgLayer = document.createElement("div"); + imgLayer.id = THEME_BG_IMG_ID; + imgLayer.style.position = "absolute"; + imgLayer.style.inset = "-40px"; + imgLayer.style.backgroundSize = "cover"; + imgLayer.style.backgroundPosition = "center"; + imgLayer.style.backgroundRepeat = "no-repeat"; + bgElement.appendChild(imgLayer); + } + + imgLayer.style.backgroundImage = `url("${theme.bgUrl}")`; + imgLayer.style.filter = theme.bgBlur ? `blur(${theme.bgBlur}px)` : ""; + imgLayer.style.opacity = + theme.bgOpacity != null ? String(theme.bgOpacity / 100) : "1"; + + let overlayLayer = document.getElementById(THEME_BG_OVERLAY_ID); + if (!overlayLayer) { + overlayLayer = document.createElement("div"); + overlayLayer.id = THEME_BG_OVERLAY_ID; + overlayLayer.style.position = "absolute"; + overlayLayer.style.inset = "0"; + bgElement.appendChild(overlayLayer); + } + + const overlayOpacity = + theme.bgOverlay != null ? Number(theme.bgOverlay) / 100 : 0; + overlayLayer.style.backgroundColor = + overlayOpacity > 0 ? `rgba(0,0,0,${overlayOpacity})` : ""; } } diff --git a/src/styles/glass.css b/src/styles/glass.css index ad55309..7b81cf6 100644 --- a/src/styles/glass.css +++ b/src/styles/glass.css @@ -8,7 +8,8 @@ box-shadow: 0px 2px 16px 0px rgba(0, 0, 0, 0.08); } -html[data-pattern-bg="true"] .glass-surface { +html[data-pattern-bg="true"] .glass-surface, +html[data-image-bg="true"] .glass-surface { -webkit-backdrop-filter: blur(1px); backdrop-filter: blur(1px); } @@ -38,7 +39,8 @@ html[data-theme="dark"] .glass-surface--selected { box-shadow: 0px -2px 14px 0px rgba(0, 0, 0, 0.15); } -html[data-pattern-bg="true"] .glass-surface-nav { +html[data-pattern-bg="true"] .glass-surface-nav, +html[data-image-bg="true"] .glass-surface-nav { -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px); }