From 4863b8a89f5a1ff24162a1da455e25762b00ad0d Mon Sep 17 00:00:00 2001 From: Adam Stankiewicz Date: Mon, 5 Jun 2023 01:21:25 -0400 Subject: [PATCH] fix: brand overrides docs: fix code example docs: add missing comma docs: update how to --- docs/how_tos/theming.md | 92 ++++++++-- src/react/hooks.js | 360 ++++++++++++++++++++++++++-------------- 2 files changed, 314 insertions(+), 138 deletions(-) diff --git a/docs/how_tos/theming.md b/docs/how_tos/theming.md index dec7eb771..52646a7d6 100644 --- a/docs/how_tos/theming.md +++ b/docs/how_tos/theming.md @@ -6,9 +6,9 @@ This document serves as a guide to using `@edx/frontend-platform` to support MFE ![overview of paragon theme loader](./assets/paragon-theme-loader.png "Paragon theme loader") -## Theme URL configuration +## Basic theme URL configuration -Paragon supports 2 mechanisms for configuring the Paragon theme URLs: +Paragon supports 2 mechanisms for configuring the Paragon theme urls: * JavaScript-based configuration via `env.config.js`. * MFE runtime configuration API via `edx-platform` @@ -16,9 +16,15 @@ Using either configuration mechanism, a `PARAGON_THEME_URLS` configuration setti ```json { - "core": "https://cdn.jsdelivr.net/npm/@edx/paragon@$paragonVersion/dist/core.css", + "core": { + "url": "https://cdn.jsdelivr.net/npm/@edx/paragon@$paragonVersion/dist/core.min.css" + }, "variants": { - "light": "https://cdn.jsdelivr.net/npm/@edx/paragon@$paragonVersion/dist/light.css" + "light": { + "url": "https://cdn.jsdelivr.net/npm/@edx/paragon@$paragonVersion/dist/light.min.css", + "default": true, + "dark": false, + } } } ``` @@ -32,16 +38,22 @@ To use this JavaScript-based configuration approach, you may set a `PARAGON_THEM ```js const config = { PARAGON_THEME_URLS: { - core: 'https://cdn.jsdelivr.net/npm/@edx/paragon@$paragonVersion/dist/paragon.css', + core: { + url: 'https://cdn.jsdelivr.net/npm/@edx/paragon@$paragonVersion/dist/core.min.css', + }, variants: { - light: 'https://cdn.jsdelivr.net/npm/@edx/paragon@$paragonVersion/scss/core/css/variables.css', + light: { + url: 'https://cdn.jsdelivr.net/npm/@edx/paragon@$paragonVersion/dist/light.min.css', + default: true, + dark: false, + }, }, }, }; + export default config; ``` - ### MFE runtime configuration API `@edx/frontend-platform` additionally supports loading application configuration from the MFE runtime configuration API via `edx-platform`. The configuration is served by the `http://localhost:18000/api/mfe_config/v1` API endpoint. For more information, refer to [this documentation](https://github.com/openedx/edx-platform/blob/master/lms/djangoapps/mfe_config_api/docs/decisions/0001-mfe-config-api.rst) about the MFE runtime configuration API, please see these docs. @@ -52,12 +64,18 @@ The application configuration may be setup via Django settings as follows: ENABLE_MFE_CONFIG_API = True MFE_CONFIG = {} MFE_CONFIG_OVERRIDES = { - # `APP_ID` defined in your MFE + # The below key represented the `APP_ID` defined in your MFE 'profile': { 'PARAGON_THEME_URLS': { - 'core': 'https://cdn.jsdelivr.net/npm/@edx/paragon@$paragonVersion/dist/core.css', + 'core': { + 'url': 'https://cdn.jsdelivr.net/npm/@edx/paragon@$paragonVersion/dist/core.min.css', + }, 'variants': { - 'light': 'https://cdn.jsdelivr.net/npm/@edx/paragon@$paragonVersion/dist/light.css', + 'light': { + 'url': 'https://cdn.jsdelivr.net/npm/@edx/paragon@$paragonVersion/dist/light.min.css', + 'default': True, + 'dark': False, + }, }, }, }, @@ -66,11 +84,57 @@ MFE_CONFIG_OVERRIDES = { ### Locally installed `@edx/paragon` -In the event the other Paragon CSS URLs are configured via one of the other documented mechanisms, but they fail to load (e.g., the CDN url throws a 404), `@edx/frontend-platform` will fallback to injecting the locally installed Paragon CSS from the consuming application into the HTML document. +If you would like to use the same version of the Paragon CSS urls as the locally installed `@edx/paragon`, the configuration for the Paragon CSS urls may contain a wildcard `$paragonVersion` which gets replaced with the locally installed version of `@edx/paragon` in the consuming application, e.g.: + +```shell +https://cdn.jsdelivr.net/npm/@edx/paragon@$paragonVersion/dist/core.min.css +https://cdn.jsdelivr.net/npm/@edx/paragon@$paragonVersion/dist/light.min.css +``` + +In the event the other Paragon CSS urls are configured via one of the other documented mechanisms, but they fail to load (e.g., the CDN url throws a 404), `@edx/frontend-platform` will attempt to fallback to injecting the locally installed Paragon CSS from the consuming application into the HTML document. + +## Usage with `@edx/brand` + +The core Paragon design tokens and styles may be optionally overriden by utilizing `@edx/brand`, which allows theme authors to customize the default Paragon theme to match the look and feel of their custom brand. -If you would like to use the same version of the Paragon CSS URLs as the locally installed `@edx/paragon`, the configuration for the Paragon CSS URLs may contain a wildcard `$paragonVersion` which gets replaced with the locally installed version of `@edx/paragon` in the consuming application, e.g.: +This override mechanism works by compiling the design tokens defined in `@edx/brand` with the the core Paragon tokens to generate overrides to Paragon's default CSS variables, and then compiling the output CSS with any SCSS theme customizations not possible through a design token override. + +The CSS urls for `@edx/brand` overrides will be applied after the core Paragon theme urls load, thus overriding any previously set CSS variables and/or styles. + +To enable `@edx/brand` overrides, the `PARAGON_THEME_URLS` setting may be configured as following: + +```js +const config = { + PARAGON_THEME_URLS: { + core: { + urls: { + default: 'https://cdn.jsdelivr.net/npm/@edx/paragon@$paragonVersion/dist/core.min.css', + brandOverride: 'https://cdn.jsdelivr.net/npm/@edx/brand-edx.org@#brandVersion/dist/core.min.css', + }, + }, + variants: { + light: { + urls: { + default: 'https://cdn.jsdelivr.net/npm/@edx/paragon@$paragonVersion/dist/light.min.css', + brandOverride: 'https://cdn.jsdelivr.net/npm/@edx/brand-edx.org@$brandVersion/dist/light.min.css', + }, + default: true, + dark: false, + }, + }, + }, +}; + +export default config; +``` + +### Locally installed `@edx/brand` + +If you would like to use the same version of the brand override CSS urls as the locally installed `@edx/brand`, the configuration for the brand override CSS urls may contain a wildcard `$brandVersion` which gets replaced with the locally installed version of `@edx/brand` in the consuming application, e.g.: ```shell -https://cdn.jsdelivr.net/npm/@edx/paragon@$paragonVersion/dist/core.css -https://cdn.jsdelivr.net/npm/@edx/paragon@$paragonVersion/scss/core/css/variables.css +https://cdn.jsdelivr.net/npm/@edx/brand@$brandVersion/dist/core.min.css +https://cdn.jsdelivr.net/npm/@edx/brand@$brandVersion/dist/light.min.css ``` + +In the event the other brand override CSS urls are configured via one of the other documented mechanisms, but they fail to load (e.g., the CDN is down), `@edx/frontend-platform` will attempt to fallback to injecting the locally installed brand override CSS urls from the consuming application into the HTML document. diff --git a/src/react/hooks.js b/src/react/hooks.js index 0302f3e5f..b31ae90a4 100644 --- a/src/react/hooks.js +++ b/src/react/hooks.js @@ -53,6 +53,12 @@ export const useTrackColorSchemeChoice = () => { }, []); }; +const removeExistingLinks = (existingLinks) => { + existingLinks.forEach((link) => { + link.remove(); + }); +}; + /** * Adds/updates a `` element in the HTML document to load the core application theme CSS. * @@ -65,63 +71,117 @@ export const useParagonThemeCore = ({ themeCore, onLoad, }) => { + const [isParagonThemeCoreLoaded, setIsParagonThemeCoreLoaded] = useState(false); + const [isBrandThemeCoreLoaded, setIsBrandThemeCoreLoaded] = useState(false); + + useEffect(() => { + // Call `onLoad` once both the paragon and brand theme core are loaded. + if (isParagonThemeCoreLoaded && isBrandThemeCoreLoaded) { + onLoad(); + } + }, [isParagonThemeCoreLoaded, isBrandThemeCoreLoaded, onLoad]); + useEffect(() => { // If there is no config for the core theme url, do nothing. - if (!themeCore?.url) { + if (!themeCore?.urls) { + setIsParagonThemeCoreLoaded(true); + setIsBrandThemeCoreLoaded(true); return; } - let coreThemeLink = document.head.querySelector(`link[href='${themeCore.url}']`); - if (!coreThemeLink) { - const getExistingCoreThemeLinks = () => document.head.querySelectorAll('link[data-paragon-theme-core="true"]'); - const removeExistingLinks = (existingLinks) => { - existingLinks.forEach((link) => { - link.remove(); - }); + const getParagonThemeCoreLink = () => document.head.querySelector('link[data-paragon-theme-core="true"'); + const existingCoreThemeLink = document.head.querySelector(`link[href='${themeCore.urls.default}']`); + if (!existingCoreThemeLink) { + const getExistingCoreThemeLinks = (isBrandOverride) => { + const coreThemeLinkSelector = `link[data-${isBrandOverride ? 'brand' : 'paragon'}-theme-core="true"]`; + return document.head.querySelectorAll(coreThemeLinkSelector); }; - // find existing links - const existingLinks = getExistingCoreThemeLinks(); - - // create new link - const createCoreThemeLink = (url, { isFallbackThemeUrl = false } = {}) => { - coreThemeLink = document.createElement('link'); + const createCoreThemeLink = ( + url, + { + isFallbackThemeUrl = false, + isBrandOverride = false, + } = {}, + ) => { + let coreThemeLink = document.createElement('link'); coreThemeLink.href = url; coreThemeLink.rel = 'stylesheet'; - coreThemeLink.dataset.paragonThemeCore = true; + if (isBrandOverride) { + coreThemeLink.dataset.brandThemeCore = true; + } else { + coreThemeLink.dataset.paragonThemeCore = true; + } coreThemeLink.onload = () => { - onLoad(); - removeExistingLinks(existingLinks); + if (isBrandOverride) { + setIsBrandThemeCoreLoaded(true); + } else { + setIsParagonThemeCoreLoaded(true); + } }; coreThemeLink.onerror = () => { logError(`Failed to load core theme CSS from ${url}`); if (isFallbackThemeUrl) { - logError('Could not load core theme CSS from fallback URL. Aborting.'); - onLoad(); - const otherExistingLinks = getExistingCoreThemeLinks(); + logError(`Could not load core theme CSS from ${url} or fallback URL. Aborting.`); + if (isBrandOverride) { + setIsBrandThemeCoreLoaded(true); + } else { + setIsParagonThemeCoreLoaded(true); + } + const otherExistingLinks = getExistingCoreThemeLinks(isBrandOverride); removeExistingLinks(otherExistingLinks); return; } - if (PARAGON?.themeUrls?.core) { - const coreThemeFallbackUrl = `${getConfig().BASE_URL}/${PARAGON.themeUrls.core.fileName}`; + const paragonThemeAccessor = isBrandOverride ? 'brand' : 'paragon'; + const themeUrls = PARAGON_THEME?.[paragonThemeAccessor]?.themeUrls ?? {}; + if (themeUrls.core) { + const coreThemeFallbackUrl = `${getConfig().BASE_URL}/${themeUrls.core.fileName}`; logInfo(`Falling back to locally installed core theme CSS: ${coreThemeFallbackUrl}`); - coreThemeLink = createCoreThemeLink(coreThemeFallbackUrl, { isFallbackThemeUrl: true }); - const otherExistingLinks = getExistingCoreThemeLinks(); + coreThemeLink = createCoreThemeLink(coreThemeFallbackUrl, { isFallbackThemeUrl: true, isBrandOverride }); + const otherExistingLinks = getExistingCoreThemeLinks(isBrandOverride); removeExistingLinks(otherExistingLinks); - document.head.insertAdjacentElement( - 'afterbegin', - coreThemeLink, - ); + const foundParagonThemCoreLink = getParagonThemeCoreLink(); + if (foundParagonThemCoreLink) { + foundParagonThemCoreLink.insertAdjacentElement( + 'afterend', + coreThemeLink, + ); + } else { + document.head.insertAdjacentElement( + 'afterbegin', + coreThemeLink, + ); + } + } else { + logError(`Failed to load core theme CSS from ${url} or fallback URL. Aborting.`); } }; return coreThemeLink; }; - console.log('createCoreThemeLink', themeCore); - coreThemeLink = createCoreThemeLink(themeCore.url); + + const paragonCoreThemeLink = createCoreThemeLink(themeCore.urls.default); document.head.insertAdjacentElement( 'afterbegin', - coreThemeLink, + paragonCoreThemeLink, ); + + if (themeCore.urls.brandOverride) { + const brandCoreThemeLink = createCoreThemeLink(themeCore.urls.brandOverride, { isBrandOverride: true }); + const foundParagonThemeCoreLink = getParagonThemeCoreLink(); + if (foundParagonThemeCoreLink) { + foundParagonThemeCoreLink.insertAdjacentElement( + 'afterend', + brandCoreThemeLink, + ); + } else { + document.head.insertAdjacentElement( + 'afterbegin', + brandCoreThemeLink, + ); + } + } else { + setIsBrandThemeCoreLoaded(true); + } } - }, [themeCore?.url, onLoad]); + }, [themeCore?.urls, onLoad]); }; /** @@ -133,8 +193,8 @@ export const useParagonThemeCore = ({ * * @memberof module:React * @param {object} args - * @param {object} args.themeVariant An object containing the URLs for each supported theme variant, e.g.: `{ light: { url: 'https://path/to/light.css' } }`. - * @param {string} args.currentThemeVariant The currently applied theme variant, e.g.: `light`. + * @param {object} [args.themeVariants] An object containing the URLs for each supported theme variant, e.g.: `{ light: { url: 'https://path/to/light.css' } }`. + * @param {string} [args.currentThemeVariant] The currently applied theme variant, e.g.: `light`. * @param {string} args.onLoad A callback function called when the theme variant(s) CSS is loaded. */ const useParagonThemeVariants = ({ @@ -142,22 +202,15 @@ const useParagonThemeVariants = ({ currentThemeVariant, onLoad, }) => { - const [hasThemeVariantLoadedByName, setHasThemeVariantLoadedByName] = useState({}); + const [isParagonThemeVariantLoaded, setIsParagonThemeVariantLoaded] = useState(false); + const [isBrandThemeVariantLoaded, setIsBrandThemeVariantLoaded] = useState(false); useEffect(() => { - Object.keys(themeVariants || {}).forEach((themeVariant) => { - setHasThemeVariantLoadedByName((prevState) => ({ - ...prevState, - [themeVariant]: false, - })); - }); - }, [themeVariants]); - - useEffect(() => { - if (Object.values(hasThemeVariantLoadedByName).some((hasLoaded) => hasLoaded)) { + // Call `onLoad` once both the paragon and brand theme variant are loaded. + if (isParagonThemeVariantLoaded && isBrandThemeVariantLoaded) { onLoad(); } - }, [hasThemeVariantLoadedByName, onLoad]); + }, [isParagonThemeVariantLoaded, isBrandThemeVariantLoaded, onLoad]); useEffect(() => { if (!themeVariants) { @@ -170,106 +223,149 @@ const useParagonThemeVariants = ({ */ const generateStylesheetRelAttr = (themeVariant) => (currentThemeVariant === themeVariant ? 'stylesheet' : 'alternate stylesheet'); - /** - * A helper function to determine which theme variant callback should be used - * based on the current theme variant. - */ - const setThemeVariantLoaded = (themeVariant) => { - setHasThemeVariantLoadedByName((prevState) => { - if (themeVariant in prevState) { - return { - ...prevState, - [themeVariant]: true, - }; - } - return prevState; - }); - }; - /** * Iterate over each theme variant URL and inject it into the HTML document, if it doesn't already exist. */ Object.entries(themeVariants).forEach(([themeVariant, value]) => { // If there is no config for the theme variant URL, set the theme variant to loaded and continue. - if (!value.url) { - setThemeVariantLoaded(themeVariant); + if (!value.urls) { + setIsParagonThemeVariantLoaded(true); + setIsBrandThemeVariantLoaded(true); return; } - let themeVariantLink = document.head.querySelector(`link[href='${value.url}']`); + const getParagonThemeVariantLink = () => document.head.querySelector(`link[data-paragon-theme-variant='${themeVariant}']`); + const existingThemeVariantLink = document.head.querySelector(`link[href='${value.urls.default}']`); const stylesheetRelForVariant = generateStylesheetRelAttr(themeVariant); - if (!themeVariantLink) { - const getExistingThemeVariantLinks = () => document.head.querySelectorAll(`link[data-paragon-theme-variant='${themeVariant}']`); - // find existing links - const existingLinks = getExistingThemeVariantLinks(); - - // create new link - const createThemeVariantLink = (url, { isFallbackThemeUrl = false } = {}) => { - themeVariantLink = document.createElement('link'); + if (!existingThemeVariantLink) { + const getExistingThemeVariantLinks = (isBrandOverride) => { + const themeVariantLinkSelector = `link[data-${isBrandOverride ? 'brand' : 'paragon'}-theme-variant='${themeVariant}']`; + return document.head.querySelectorAll(themeVariantLinkSelector); + }; + const createThemeVariantLink = ( + url, + { + isFallbackThemeUrl = false, + isBrandOverride = false, + } = {}, + ) => { + let themeVariantLink = document.createElement('link'); themeVariantLink.href = url; themeVariantLink.rel = 'stylesheet'; - themeVariantLink.dataset.paragonThemeVariant = themeVariant; + if (isBrandOverride) { + themeVariantLink.dataset.brandThemeVariant = themeVariant; + } else { + themeVariantLink.dataset.paragonThemeVariant = themeVariant; + } themeVariantLink.onload = () => { - setThemeVariantLoaded(themeVariant); - existingLinks.forEach((link) => { - link.remove(); - }); + if (themeVariant === currentThemeVariant) { + if (isBrandOverride) { + setIsBrandThemeVariantLoaded(true); + } else { + setIsParagonThemeVariantLoaded(true); + } + } else { + const existingLinks = getExistingThemeVariantLinks(isBrandOverride); + removeExistingLinks(existingLinks); + } }; themeVariantLink.onerror = () => { - logError(`Failed to load theme variant (${themeVariant}) CSS from ${value.url}`); + logError(`Failed to load theme variant (${themeVariant}) CSS from ${value.urls.default}`); if (isFallbackThemeUrl) { - logError('Could not load theme theme variant CSS from fallback URL. Aborting.'); - setThemeVariantLoaded(themeVariant); - const otherExistingLinks = getExistingThemeVariantLinks(); - otherExistingLinks.forEach((link) => { - link.remove(); - }); + logError(`Could not load theme variant (${themeVariant}) CSS from fallback URL. Aborting.`); + if (isBrandOverride) { + setIsBrandThemeVariantLoaded(true); + } else { + setIsParagonThemeVariantLoaded(true); + } + const otherExistingLinks = getExistingThemeVariantLinks(isBrandOverride); + removeExistingLinks(otherExistingLinks); return; } - if (PARAGON?.themeUrls?.variants?.[themeVariant]) { - const themeVariantFallbackUrl = `${getConfig().BASE_URL}/${PARAGON.themeUrls.variants[themeVariant].fileName}`; + const paragonThemeAccessor = isBrandOverride ? 'brand' : 'paragon'; + const themeUrls = PARAGON_THEME?.[paragonThemeAccessor]?.themeUrls ?? {}; + if (themeUrls.variants) { + const themeVariantFallbackUrl = `${getConfig().BASE_URL}/${themeUrls.variants[themeVariant].fileName}`; logInfo(`Falling back to locally installed theme variant (${themeVariant}) CSS: ${themeVariantFallbackUrl}`); - themeVariantLink = createThemeVariantLink(themeVariantFallbackUrl, { isFallbackThemeUrl: true }); - const otherExistingLinks = getExistingThemeVariantLinks(); - otherExistingLinks.forEach((link) => { - link.remove(); + themeVariantLink = createThemeVariantLink(themeVariantFallbackUrl, { + isFallbackThemeUrl: true, + isBrandOverride, }); - document.head.insertAdjacentElement( - 'afterbegin', - themeVariantLink, - ); + const otherExistingLinks = getExistingThemeVariantLinks(isBrandOverride); + removeExistingLinks(otherExistingLinks); + const foundParagonThemeVariantLink = getParagonThemeVariantLink(); + if (foundParagonThemeVariantLink) { + foundParagonThemeVariantLink.insertAdjacentElement( + 'afterend', + themeVariantLink, + ); + } else { + document.head.insertAdjacentElement( + 'afterbegin', + themeVariantLink, + ); + } + } else { + logError(`Failed to load theme variant (${themeVariant}) CSS from ${url} or fallback URL. Aborting.`); } }; return themeVariantLink; }; - themeVariantLink = createThemeVariantLink(value.url); + + const paragonThemeVariantLink = createThemeVariantLink(value.urls.default); document.head.insertAdjacentElement( 'afterbegin', - themeVariantLink, + paragonThemeVariantLink, ); - } else if (themeVariantLink.rel !== stylesheetRelForVariant) { - themeVariantLink.rel = stylesheetRelForVariant; + + if (value.urls.brandOverride) { + const brandThemeVariantLink = createThemeVariantLink(value.urls.brandOverride, { isBrandOverride: true }); + const foundParagonThemeVariantLink = getParagonThemeVariantLink(); + if (foundParagonThemeVariantLink) { + foundParagonThemeVariantLink.insertAdjacentElement( + 'afterend', + brandThemeVariantLink, + ); + } else { + document.head.insertAdjacentElement( + 'afterbegin', + brandThemeVariantLink, + ); + } + } else { + setIsBrandThemeVariantLoaded(true); + } + } else if (existingThemeVariantLink.rel !== stylesheetRelForVariant) { + existingThemeVariantLink.rel = stylesheetRelForVariant; } }); }, [themeVariants, currentThemeVariant, onLoad]); }; -const handleParagonVersionSubstitution = (url) => { - if (!url || !url.includes('$paragonVersion') || !PARAGON?.version) { +const handleParagonVersionSubstitution = (url, { isBrandOverride = false } = {}) => { + const localVersion = isBrandOverride ? PARAGON_THEME?.brand?.version : PARAGON_THEME?.paragon?.version; + const wildcardKeyword = isBrandOverride ? '$brandVersion' : '$paragonVersion'; + if (!url || !url.includes(wildcardKeyword) || !localVersion) { return url; } - return url.replace('$paragonVersion', PARAGON.version); + return url.replace(wildcardKeyword, localVersion); }; +/** + * @typedef {Object} ParagonThemeUrl + * @property {string} default The default URL for Paragon. + * @property {string} brandOverride The URL for a brand override. + */ + /** * @typedef {Object} ParagonThemeCore - * @property {string} url + * @property {ParagonThemeUrl|string} url */ /** * @typedef {Object} ParagonThemeVariant - * @property {string} url - * @property {boolean} default - * @property {boolean} dark + * @property {ParagonThemeUrl|string} url + * @property {boolean} default Whether this is the default theme variant. + * @property {boolean} dark Whether this is the dark theme variant to use for `prefers-color-scheme: "dark"`. */ /** @@ -288,42 +384,58 @@ const useParagonThemeUrls = (config) => useMemo(() => { if (!config.PARAGON_THEME_URLS) { return undefined; } + /** @type {ParagonThemeUrls} */ const paragonThemeUrls = config.PARAGON_THEME_URLS; - const coreCssUrl = handleParagonVersionSubstitution(paragonThemeUrls.core); + const paragonCoreCssUrl = typeof paragonThemeUrls.core.urls === 'object' ? paragonThemeUrls.core.urls.default : paragonThemeUrls.core.url; + const brandCoreCssUrl = typeof paragonThemeUrls.core.urls === 'object' ? paragonThemeUrls.core.urls.brandOverride : undefined; + const coreCss = { + default: handleParagonVersionSubstitution(paragonCoreCssUrl), + brandOverride: handleParagonVersionSubstitution(brandCoreCssUrl, { isBrandOverride: true }), + }; const themeVariantsCss = {}; - Object.entries(paragonThemeUrls.variants || {}).forEach(([themeVariant, { url, ...rest }]) => { - themeVariantsCss[themeVariant] = { - url: handleParagonVersionSubstitution(url), - ...rest, + const themeVariantsEntries = Object.entries(paragonThemeUrls.variants || {}); + themeVariantsEntries.forEach(([themeVariant, { + url, urls, default: isDefaultThemeVariant, dark, + }]) => { + const themeVariantMetadata = { + default: isDefaultThemeVariant, + dark, }; + if (url) { + themeVariantMetadata.urls = { + default: handleParagonVersionSubstitution(url), + }; + } else { + themeVariantMetadata.urls = { + default: handleParagonVersionSubstitution(urls.default), + brandOverride: handleParagonVersionSubstitution(urls.brandOverride, { isBrandOverride: true }), + }; + } + themeVariantsCss[themeVariant] = themeVariantMetadata; }); - const hasMissingCssUrls = !coreCssUrl || Object.keys(themeVariantsCss).length === 0; + const hasMissingCssUrls = !coreCss.default || Object.keys(themeVariantsCss).length === 0; if (hasMissingCssUrls) { - if (!PARAGON) { + if (!PARAGON_THEME) { return undefined; } const themeVariants = {}; const prependBaseUrl = (url) => `${config.BASE_URL}/${url}`; - Object.entries(PARAGON.themeUrls.variants).forEach(([themeVariant, { fileName, ...rest }]) => { + themeVariantsEntries.forEach(([themeVariant, { fileName, ...rest }]) => { themeVariants[themeVariant] = { url: prependBaseUrl(fileName), ...rest, }; }); return { - core: { - url: prependBaseUrl(PARAGON.themeUrls.core), - }, + core: { urls: coreCss }, variants: themeVariants, }; } return { - core: { - url: handleParagonVersionSubstitution(coreCssUrl), - }, + core: { urls: coreCss }, variants: themeVariantsCss, }; }, [config.BASE_URL, config.PARAGON_THEME_URLS]); @@ -349,8 +461,8 @@ const getDefaultThemeVariant = (themeVariants) => { metadata: themeVariants[themeVariantKeys[0]], }; } - const foundDefaultThemeVariant = Object.entries(themeVariants) - .find(([, { default: isDefault }]) => isDefault === true); + const foundDefaultThemeVariant = Object.values(themeVariants) + .find(({ default: isDefault }) => isDefault === true); if (!foundDefaultThemeVariant) { return undefined; @@ -416,7 +528,7 @@ export const useParagonTheme = (config) => { return; } - const hasThemeConfig = (themeCore?.url && Object.keys(themeVariants).length > 0); + const hasThemeConfig = (themeCore?.urls && Object.keys(themeVariants).length > 0); if (!hasThemeConfig) { // no theme URLs to load, set loading to false. dispatch(paragonThemeActions.setParagonThemeLoaded(true)); @@ -433,7 +545,7 @@ export const useParagonTheme = (config) => { themeState.isThemeLoaded, isCoreThemeLoaded, hasLoadedThemeVariants, - themeCore?.url, + themeCore?.urls, themeVariants, ]);