export const appearanceThemeModes = ["light", "dark", "system"] as const; export const appearanceFontFamilies = ["marianne", "system", "rounded", "serif"] as const; export const appearanceCardStyles = ["soft", "outlined", "elevated"] as const; export const appearanceTableStyles = ["comfortable", "compact", "striped"] as const; export const appearanceDisplayDensities = ["comfortable", "compact", "detailed"] as const; export const appearanceSidebarStyles = ["soft", "contrast", "floating"] as const; export const appearanceButtonStyles = ["rounded", "pill", "square"] as const; export const appearanceInputStyles = ["outline", "soft", "filled"] as const; export const appearanceBackgroundModes = ["cover", "repeat", "fixed"] as const; export const appearanceBackgroundPresentationModes = ["full", "edge_frame"] as const; export const appearanceDecorationScopes = [ "all", "home", "public", "public_directory", "public_map", "dashboard", "dashboard_home", "dashboard_profile", "dashboard_documents", "dashboard_requests", "dashboard_new_request", "dashboard_reservation_salle", "dashboard_material", "dashboard_directory", "admin", "admin_pilotage", "admin_logistics", "admin_directory", "admin_compliance", ] as const; export const appearanceDecorationLayers = ["background", "card"] as const; export const appearanceCardDecorationAnchors = ["top_left", "top_right", "bottom_left", "bottom_right"] as const; export const appearanceBackgroundPositions = [ "center", "top", "bottom", "left", "right", "top_left", "top_right", "bottom_left", "bottom_right", ] as const; export const appearanceBackgroundTargets = [ "home", "public", "public_directory", "public_map", "dashboard", "dashboard_home", "dashboard_profile", "dashboard_documents", "dashboard_requests", "dashboard_new_request", "dashboard_reservation_salle", "dashboard_material", "dashboard_directory", "admin", "admin_pilotage", "admin_logistics", "admin_directory", "admin_compliance", ] as const; export const appearanceAssetFields = ["logoUrl", "faviconUrl", "heroImageUrl", "backgroundImageUrl"] as const; export type AppearanceThemeMode = (typeof appearanceThemeModes)[number]; export type AppearanceFontFamily = (typeof appearanceFontFamilies)[number]; export type AppearanceCardStyle = (typeof appearanceCardStyles)[number]; export type AppearanceTableStyle = (typeof appearanceTableStyles)[number]; export type AppearanceDisplayDensity = (typeof appearanceDisplayDensities)[number]; export type AppearanceSidebarStyle = (typeof appearanceSidebarStyles)[number]; export type AppearanceButtonStyle = (typeof appearanceButtonStyles)[number]; export type AppearanceInputStyle = (typeof appearanceInputStyles)[number]; export type AppearanceBackgroundMode = (typeof appearanceBackgroundModes)[number]; export type AppearanceBackgroundPresentationMode = (typeof appearanceBackgroundPresentationModes)[number]; export type AppearanceDecorationScope = (typeof appearanceDecorationScopes)[number]; export type AppearanceDecorationLayer = (typeof appearanceDecorationLayers)[number]; export type AppearanceCardDecorationAnchor = (typeof appearanceCardDecorationAnchors)[number]; export type AppearanceBackgroundPosition = (typeof appearanceBackgroundPositions)[number]; export type AppearanceBackgroundTarget = (typeof appearanceBackgroundTargets)[number]; export type AppearanceAssetField = (typeof appearanceAssetFields)[number]; export interface PortalAppearanceDecoration { id: string; imageUrl: string; scope: AppearanceDecorationScope; layer: AppearanceDecorationLayer; widthPercent: number; topPercent: number; leftPercent: number; opacity: number; blurRadius: number; cardAnchor: AppearanceCardDecorationAnchor; cardInsetPercent: number; } export interface PortalAppearance { themeMode: AppearanceThemeMode; activeSkinId: string; portalTitle: string; portalTagline: string; primaryColor: string; secondaryColor: string; accentColor: string; buttonColor: string; alertColor: string; fontFamily: AppearanceFontFamily; headingFontFamily: AppearanceFontFamily; headingScalePercent: number; bodyScalePercent: number; logoUrl: string; faviconUrl: string; heroImageUrl: string; backgroundImageUrl: string; backgroundEnabled: boolean; backgroundMode: AppearanceBackgroundMode; backgroundPresentationMode: AppearanceBackgroundPresentationMode; backgroundOverlayOpacity: number; backgroundBlurRadius: number; backgroundTargets: AppearanceBackgroundTarget[]; backgroundTargetImageUrls: Partial>; backgroundTargetOverlayOpacities: Partial>; backgroundTargetBlurRadii: Partial>; backgroundTargetPositions: Partial>; backgroundTargetPresentationModes: Partial>; heroImagePositionX: number; heroImagePositionY: number; heroOverlayOpacity: number; heroTitlePositionX: number; heroTitlePositionY: number; heroTitleWidthPercent: number; heroTaglinePositionX: number; heroTaglinePositionY: number; heroTaglineWidthPercent: number; decorativeElements: PortalAppearanceDecoration[]; cardStyle: AppearanceCardStyle; tableStyle: AppearanceTableStyle; displayDensity: AppearanceDisplayDensity; sidebarStyle: AppearanceSidebarStyle; buttonStyle: AppearanceButtonStyle; inputStyle: AppearanceInputStyle; } export interface AppearanceSkinPreset { id: string; label: string; description: string; category: string; usage: string[]; value: Partial; } export const APPEARANCE_SETTING_KEY = "appearance"; export const defaultPortalAppearance: PortalAppearance = { themeMode: "light", activeSkinId: "guyane-institutionnelle", portalTitle: "Portail Associations 973", portalTagline: "Démarches, annuaire et services du tissu associatif guyanais", primaryColor: "#2e7d32", secondaryColor: "#1565c0", accentColor: "#f9a825", buttonColor: "#1565c0", alertColor: "#dc2626", fontFamily: "marianne", headingFontFamily: "marianne", headingScalePercent: 100, bodyScalePercent: 100, logoUrl: "", faviconUrl: "", heroImageUrl: "", backgroundImageUrl: "", backgroundEnabled: false, backgroundMode: "cover", backgroundPresentationMode: "full", backgroundOverlayOpacity: 72, backgroundBlurRadius: 0, backgroundTargets: ["home", "public", "dashboard", "admin"], backgroundTargetImageUrls: {}, backgroundTargetOverlayOpacities: {}, backgroundTargetBlurRadii: {}, backgroundTargetPositions: {}, backgroundTargetPresentationModes: {}, heroImagePositionX: 50, heroImagePositionY: 50, heroOverlayOpacity: 74, heroTitlePositionX: 0, heroTitlePositionY: 0, heroTitleWidthPercent: 50, heroTaglinePositionX: 0, heroTaglinePositionY: 0, heroTaglineWidthPercent: 80, decorativeElements: [], cardStyle: "soft", tableStyle: "comfortable", displayDensity: "comfortable", sidebarStyle: "soft", buttonStyle: "rounded", inputStyle: "outline", }; export const appearanceSkinPresets: AppearanceSkinPreset[] = [ { id: "guyane-institutionnelle", label: "Guyane institutionnelle", description: "Palette ancrée dans le territoire, pensée pour les collectivités, services publics et fédérations.", category: "Institutionnel", usage: ["collectivités", "mairies", "fédérations", "services publics"], value: { activeSkinId: "guyane-institutionnelle", portalTitle: "Portail Associations 973", portalTagline: "L’espace territorial des démarches associatives en Guyane", primaryColor: "#2e7d32", secondaryColor: "#1565c0", accentColor: "#f9a825", buttonColor: "#1565c0", alertColor: "#c62828", fontFamily: "marianne", headingFontFamily: "marianne", cardStyle: "soft", tableStyle: "comfortable", displayDensity: "comfortable", sidebarStyle: "soft", buttonStyle: "rounded", inputStyle: "outline", backgroundEnabled: false, backgroundMode: "cover", backgroundPresentationMode: "full", backgroundOverlayOpacity: 72, backgroundBlurRadius: 0, backgroundTargets: ["home", "public", "dashboard", "admin"], backgroundTargetImageUrls: {}, themeMode: "light", }, }, { id: "centre-spatial-guyanais", label: "Centre spatial guyanais", description: "Ambiance technologique et institutionnelle, inspirée du spatial, de l’innovation et des grands projets.", category: "Spatial", usage: ["innovation", "recherche", "éducation", "projets scientifiques"], value: { activeSkinId: "centre-spatial-guyanais", portalTitle: "Portail Associations 973", portalTagline: "Innovation, projets et coopérations du territoire guyanais", primaryColor: "#0b3d91", secondaryColor: "#1f5fbf", accentColor: "#f6b317", buttonColor: "#155eef", alertColor: "#d92d20", fontFamily: "system", headingFontFamily: "rounded", cardStyle: "elevated", tableStyle: "striped", displayDensity: "compact", sidebarStyle: "contrast", buttonStyle: "pill", inputStyle: "soft", backgroundEnabled: false, backgroundMode: "cover", backgroundPresentationMode: "full", backgroundOverlayOpacity: 68, backgroundBlurRadius: 0, backgroundTargets: ["home", "public", "dashboard", "admin"], backgroundTargetImageUrls: {}, themeMode: "light", }, }, { id: "nature-amazonienne", label: "Nature amazonienne", description: "Univers plus organique, centré sur la biodiversité, l’environnement et les initiatives de terrain.", category: "Territoire", usage: ["environnement", "tourisme", "biodiversité", "éducation populaire"], value: { activeSkinId: "nature-amazonienne", portalTitle: "Portail Associations 973", portalTagline: "Les initiatives guyanaises entre nature, territoire et engagement", primaryColor: "#1b5e20", secondaryColor: "#4caf50", accentColor: "#0277bd", buttonColor: "#2e7d32", alertColor: "#d84315", fontFamily: "serif", headingFontFamily: "serif", cardStyle: "outlined", tableStyle: "comfortable", displayDensity: "detailed", sidebarStyle: "floating", buttonStyle: "rounded", inputStyle: "filled", backgroundEnabled: false, backgroundMode: "cover", backgroundPresentationMode: "full", backgroundOverlayOpacity: 70, backgroundBlurRadius: 0, backgroundTargets: ["home", "public", "dashboard", "admin"], backgroundTargetImageUrls: {}, themeMode: "light", }, }, { id: "republicain", label: "Républicain", description: "Lecture claire, signal institutionnel fort, pour les espaces administratifs et les usages plus officiels.", category: "Administration", usage: ["préfecture", "administration", "services publics", "collectivités"], value: { activeSkinId: "republicain", portalTitle: "Portail Associations 973", portalTagline: "Un portail administratif lisible, fiable et immédiatement identifiable", primaryColor: "#0055a4", secondaryColor: "#ffffff", accentColor: "#ef4135", buttonColor: "#0055a4", alertColor: "#b42318", fontFamily: "marianne", headingFontFamily: "marianne", cardStyle: "outlined", tableStyle: "compact", displayDensity: "compact", sidebarStyle: "contrast", buttonStyle: "square", inputStyle: "outline", backgroundEnabled: false, backgroundMode: "cover", backgroundPresentationMode: "full", backgroundOverlayOpacity: 78, backgroundBlurRadius: 0, backgroundTargets: ["home", "public", "dashboard", "admin"], backgroundTargetImageUrls: {}, themeMode: "light", }, }, { id: "institution-immersif", label: "Institution immersif", description: "Preset prêt à l’emploi pour un fond premium lisible, avec centre neutre et bords illustrés renforcés.", category: "Immersif", usage: ["dashboard", "portail institutionnel", "parcours connectés", "habillage premium"], value: { activeSkinId: "institution-immersif", portalTitle: "Portail Associations 973", portalTagline: "Un portail territorial clair, immersif et immédiatement lisible", primaryColor: "#2e7d32", secondaryColor: "#1565c0", accentColor: "#f9a825", buttonColor: "#1565c0", alertColor: "#c62828", fontFamily: "marianne", headingFontFamily: "marianne", cardStyle: "soft", tableStyle: "comfortable", displayDensity: "compact", sidebarStyle: "soft", buttonStyle: "rounded", inputStyle: "outline", backgroundEnabled: true, backgroundMode: "cover", backgroundPresentationMode: "edge_frame", backgroundOverlayOpacity: 62, backgroundBlurRadius: 0, backgroundTargets: ["home", "public", "dashboard", "admin"], backgroundTargetImageUrls: {}, backgroundTargetOverlayOpacities: { home: 58, public: 64, dashboard: 60, admin: 68, }, backgroundTargetPositions: { home: "center", public: "center", dashboard: "right", admin: "right", }, backgroundTargetPresentationModes: { home: "edge_frame", public: "edge_frame", dashboard: "edge_frame", admin: "edge_frame", }, themeMode: "light", }, }, { id: "parcours-guide-immersif", label: "Parcours guidé immersif", description: "Met en avant les parcours, les formulaires et les cartes avec un habillage plus narratif et très lisible.", category: "Immersif", usage: ["nouvelle demande", "réservation", "matériel", "parcours guidés"], value: { activeSkinId: "parcours-guide-immersif", portalTitle: "Portail Associations 973", portalTagline: "Des démarches plus claires, étape par étape", primaryColor: "#2f9e44", secondaryColor: "#1d4ed8", accentColor: "#f6c453", buttonColor: "#2563eb", alertColor: "#c2410c", fontFamily: "marianne", headingFontFamily: "rounded", cardStyle: "soft", tableStyle: "comfortable", displayDensity: "compact", sidebarStyle: "floating", buttonStyle: "pill", inputStyle: "soft", backgroundEnabled: true, backgroundMode: "cover", backgroundPresentationMode: "edge_frame", backgroundOverlayOpacity: 56, backgroundBlurRadius: 1, backgroundTargets: [ "dashboard", "dashboard_home", "dashboard_new_request", "dashboard_reservation_salle", "dashboard_material", ], backgroundTargetImageUrls: {}, backgroundTargetOverlayOpacities: { dashboard: 58, dashboard_home: 54, dashboard_new_request: 52, dashboard_reservation_salle: 56, dashboard_material: 56, }, backgroundTargetPositions: { dashboard: "right", dashboard_home: "right", dashboard_new_request: "right", dashboard_reservation_salle: "bottom_right", dashboard_material: "bottom_left", }, backgroundTargetPresentationModes: { dashboard: "edge_frame", dashboard_home: "edge_frame", dashboard_new_request: "edge_frame", dashboard_reservation_salle: "edge_frame", dashboard_material: "edge_frame", }, themeMode: "light", }, }, { id: "pilotage-territorial", label: "Pilotage territorial", description: "Preset plus sobre et plus dense, pensé pour le bordereau, la logistique et les référentiels admin.", category: "Immersif", usage: ["bordereau", "logistique", "référentiels", "pilotage admin"], value: { activeSkinId: "pilotage-territorial", portalTitle: "Portail Associations 973", portalTagline: "Référentiels, logistique et pilotage du territoire", primaryColor: "#256f5c", secondaryColor: "#0f4c81", accentColor: "#d97706", buttonColor: "#0f4c81", alertColor: "#b91c1c", fontFamily: "marianne", headingFontFamily: "marianne", cardStyle: "outlined", tableStyle: "compact", displayDensity: "compact", sidebarStyle: "soft", buttonStyle: "rounded", inputStyle: "outline", backgroundEnabled: true, backgroundMode: "cover", backgroundPresentationMode: "edge_frame", backgroundOverlayOpacity: 68, backgroundBlurRadius: 0, backgroundTargets: ["admin", "admin_pilotage", "admin_logistics", "admin_directory", "admin_compliance"], backgroundTargetImageUrls: {}, backgroundTargetOverlayOpacities: { admin: 68, admin_pilotage: 70, admin_logistics: 66, admin_directory: 64, admin_compliance: 72, }, backgroundTargetPositions: { admin: "right", admin_pilotage: "center", admin_logistics: "bottom_right", admin_directory: "left", admin_compliance: "top_right", }, backgroundTargetPresentationModes: { admin: "edge_frame", admin_pilotage: "edge_frame", admin_logistics: "edge_frame", admin_directory: "edge_frame", admin_compliance: "edge_frame", }, themeMode: "light", }, }, ]; const HEX_COLOR_REGEX = /^#(?:[0-9a-fA-F]{3}){1,2}$/; function normalizeHexColor(value: string | undefined, fallback: string) { const trimmed = value?.trim(); if (!trimmed || !HEX_COLOR_REGEX.test(trimmed)) { return fallback; } if (trimmed.length === 4) { return `#${trimmed[1]}${trimmed[1]}${trimmed[2]}${trimmed[2]}${trimmed[3]}${trimmed[3]}`.toLowerCase(); } return trimmed.toLowerCase(); } function normalizeUrl(value: string | undefined) { return value?.trim() || ""; } function normalizeText(value: string | undefined, fallback: string, maxLength: number) { const trimmed = value?.trim(); if (!trimmed) { return fallback; } return trimmed.slice(0, maxLength); } function normalizePercent(value: number | undefined, fallback: number, min = 0, max = 100) { if (typeof value !== "number" || Number.isNaN(value)) { return fallback; } return Math.min(max, Math.max(min, Math.round(value))); } function normalizeOption(value: string | undefined, allowed: T, fallback: T[number]) { if (value && allowed.includes(value)) { return value as T[number]; } return fallback; } function normalizeOptionsArray(value: string[] | undefined, allowed: T, fallback: readonly T[number][]) { if (!Array.isArray(value)) { return [...fallback] as T[number][]; } const filtered = value.filter((entry): entry is T[number] => allowed.includes(entry as T[number])); return filtered.length ? Array.from(new Set(filtered)) : [...fallback] as T[number][]; } function normalizeTargetImageUrls( value: Partial> | undefined, allowed: readonly AppearanceBackgroundTarget[], ) { const next: Partial> = {}; if (!value || typeof value !== "object") { return next; } for (const target of allowed) { const normalized = normalizeUrl(value[target]); if (normalized) { next[target] = normalized; } } return next; } function normalizeTargetOverlayOpacities( value: Partial> | undefined, allowed: readonly AppearanceBackgroundTarget[], ) { const next: Partial> = {}; if (!value || typeof value !== "object") { return next; } for (const target of allowed) { const raw = value[target]; if (typeof raw === "number" && !Number.isNaN(raw)) { next[target] = Math.min(100, Math.max(0, Math.round(raw))); } } return next; } function normalizeTargetBlurRadii( value: Partial> | undefined, allowed: readonly AppearanceBackgroundTarget[], ) { const next: Partial> = {}; if (!value || typeof value !== "object") { return next; } for (const target of allowed) { const raw = value[target]; if (typeof raw === "number" && !Number.isNaN(raw)) { next[target] = Math.min(24, Math.max(0, Math.round(raw))); } } return next; } function normalizeTargetPositions( value: Partial> | undefined, allowed: readonly AppearanceBackgroundTarget[], ) { const next: Partial> = {}; if (!value || typeof value !== "object") { return next; } for (const target of allowed) { const raw = value[target]; if (raw && appearanceBackgroundPositions.includes(raw as AppearanceBackgroundPosition)) { next[target] = raw as AppearanceBackgroundPosition; } } return next; } function normalizeTargetPresentationModes( value: Partial> | undefined, allowed: readonly AppearanceBackgroundTarget[], ) { const next: Partial> = {}; if (!value || typeof value !== "object") { return next; } for (const target of allowed) { const raw = value[target]; if (raw && appearanceBackgroundPresentationModes.includes(raw as AppearanceBackgroundPresentationMode)) { next[target] = raw as AppearanceBackgroundPresentationMode; } } return next; } function normalizeDecorativeElements(value: unknown): PortalAppearanceDecoration[] { if (!Array.isArray(value)) { return []; } return value .map((entry, index) => { const raw = entry && typeof entry === "object" ? entry as Partial : {}; const imageUrl = normalizeUrl(raw.imageUrl); if (!imageUrl) { return null; } return { id: normalizeText(raw.id, `decor-${index + 1}`, 80), imageUrl, scope: normalizeOption(raw.scope, appearanceDecorationScopes, "all"), layer: normalizeOption(raw.layer, appearanceDecorationLayers, "background"), widthPercent: normalizePercent(raw.widthPercent, 18, 6, 38), topPercent: normalizePercent(raw.topPercent, 20, 0, 100), leftPercent: normalizePercent(raw.leftPercent, 10, 0, 100), opacity: normalizePercent(raw.opacity, 12, 4, 28), blurRadius: normalizePercent(raw.blurRadius, 2, 0, 12), cardAnchor: normalizeOption(raw.cardAnchor, appearanceCardDecorationAnchors, "top_right"), cardInsetPercent: normalizePercent(raw.cardInsetPercent, 6, 0, 20), } satisfies PortalAppearanceDecoration; }) .filter((entry): entry is PortalAppearanceDecoration => Boolean(entry)) .slice(0, 6); } export function getAppearanceSkinPreset(presetId: string | undefined) { return appearanceSkinPresets.find((preset) => preset.id === presetId) || null; } export function sanitizePortalAppearance(value?: Partial | null): PortalAppearance { const preset = getAppearanceSkinPreset(value?.activeSkinId) || getAppearanceSkinPreset(defaultPortalAppearance.activeSkinId); const presetValue = preset?.value || {}; const base: PortalAppearance = { ...defaultPortalAppearance, ...presetValue, }; const normalizedBackgroundImageUrl = normalizeUrl(value?.backgroundImageUrl); const normalizedBackgroundTargetImageUrls = normalizeTargetImageUrls(value?.backgroundTargetImageUrls, appearanceBackgroundTargets); const normalizedBackgroundTargetOverlayOpacities = normalizeTargetOverlayOpacities( value?.backgroundTargetOverlayOpacities, appearanceBackgroundTargets ); const normalizedBackgroundTargetBlurRadii = normalizeTargetBlurRadii( value?.backgroundTargetBlurRadii, appearanceBackgroundTargets ); const normalizedBackgroundTargetPositions = normalizeTargetPositions( value?.backgroundTargetPositions, appearanceBackgroundTargets ); const normalizedBackgroundTargetPresentationModes = normalizeTargetPresentationModes( value?.backgroundTargetPresentationModes, appearanceBackgroundTargets ); const normalizedDecorativeElements = normalizeDecorativeElements(value?.decorativeElements); const hasAnyBackgroundAsset = Boolean(normalizedBackgroundImageUrl) || Object.values(normalizedBackgroundTargetImageUrls).some(Boolean); return { themeMode: normalizeOption(value?.themeMode, appearanceThemeModes, base.themeMode), activeSkinId: normalizeText(value?.activeSkinId, base.activeSkinId, 80), portalTitle: normalizeText(value?.portalTitle, base.portalTitle, 120), portalTagline: normalizeText(value?.portalTagline, base.portalTagline, 220), primaryColor: normalizeHexColor(value?.primaryColor, base.primaryColor), secondaryColor: normalizeHexColor(value?.secondaryColor, base.secondaryColor), accentColor: normalizeHexColor(value?.accentColor, base.accentColor), buttonColor: normalizeHexColor(value?.buttonColor, base.buttonColor), alertColor: normalizeHexColor(value?.alertColor, base.alertColor), fontFamily: normalizeOption(value?.fontFamily, appearanceFontFamilies, base.fontFamily), headingFontFamily: normalizeOption(value?.headingFontFamily, appearanceFontFamilies, base.headingFontFamily), headingScalePercent: normalizePercent(value?.headingScalePercent, base.headingScalePercent, 85, 130), bodyScalePercent: normalizePercent(value?.bodyScalePercent, base.bodyScalePercent, 90, 115), logoUrl: normalizeUrl(value?.logoUrl), faviconUrl: normalizeUrl(value?.faviconUrl), heroImageUrl: normalizeUrl(value?.heroImageUrl), backgroundImageUrl: normalizedBackgroundImageUrl, backgroundEnabled: Boolean(value?.backgroundEnabled) && hasAnyBackgroundAsset, backgroundMode: normalizeOption(value?.backgroundMode, appearanceBackgroundModes, base.backgroundMode), backgroundPresentationMode: normalizeOption( value?.backgroundPresentationMode, appearanceBackgroundPresentationModes, base.backgroundPresentationMode ), backgroundOverlayOpacity: normalizePercent(value?.backgroundOverlayOpacity, base.backgroundOverlayOpacity, 0, 100), backgroundBlurRadius: normalizePercent(value?.backgroundBlurRadius, base.backgroundBlurRadius, 0, 24), backgroundTargets: normalizeOptionsArray(value?.backgroundTargets, appearanceBackgroundTargets, base.backgroundTargets), backgroundTargetImageUrls: normalizedBackgroundTargetImageUrls, backgroundTargetOverlayOpacities: normalizedBackgroundTargetOverlayOpacities, backgroundTargetBlurRadii: normalizedBackgroundTargetBlurRadii, backgroundTargetPositions: normalizedBackgroundTargetPositions, backgroundTargetPresentationModes: normalizedBackgroundTargetPresentationModes, heroImagePositionX: normalizePercent(value?.heroImagePositionX, base.heroImagePositionX), heroImagePositionY: normalizePercent(value?.heroImagePositionY, base.heroImagePositionY), heroOverlayOpacity: normalizePercent(value?.heroOverlayOpacity, base.heroOverlayOpacity), heroTitlePositionX: normalizePercent(value?.heroTitlePositionX, base.heroTitlePositionX, -50, 50), heroTitlePositionY: normalizePercent(value?.heroTitlePositionY, base.heroTitlePositionY, -50, 50), heroTitleWidthPercent: normalizePercent(value?.heroTitleWidthPercent, base.heroTitleWidthPercent, 30, 100), heroTaglinePositionX: normalizePercent(value?.heroTaglinePositionX, base.heroTaglinePositionX, -50, 50), heroTaglinePositionY: normalizePercent(value?.heroTaglinePositionY, base.heroTaglinePositionY, -50, 50), heroTaglineWidthPercent: normalizePercent(value?.heroTaglineWidthPercent, base.heroTaglineWidthPercent, 30, 100), decorativeElements: normalizedDecorativeElements, cardStyle: normalizeOption(value?.cardStyle, appearanceCardStyles, base.cardStyle), tableStyle: normalizeOption(value?.tableStyle, appearanceTableStyles, base.tableStyle), displayDensity: normalizeOption(value?.displayDensity, appearanceDisplayDensities, base.displayDensity), sidebarStyle: normalizeOption(value?.sidebarStyle, appearanceSidebarStyles, base.sidebarStyle), buttonStyle: normalizeOption(value?.buttonStyle, appearanceButtonStyles, base.buttonStyle), inputStyle: normalizeOption(value?.inputStyle, appearanceInputStyles, base.inputStyle), }; }