diff --git a/app/javascript/portal/portalHelpers.js b/app/javascript/portal/portalHelpers.js
index 6c0a87ffc..8b36e63f9 100644
--- a/app/javascript/portal/portalHelpers.js
+++ b/app/javascript/portal/portalHelpers.js
@@ -4,7 +4,7 @@ import Vue from 'vue';
import PublicArticleSearch from './components/PublicArticleSearch.vue';
import TableOfContents from './components/TableOfContents.vue';
-export const getHeadingsfromTheArticle = () => {
+export const getHeadingsFromTheArticle = () => {
const rows = [];
const articleElement = document.getElementById('cw-article-content');
articleElement.querySelectorAll('h1, h2, h3').forEach(element => {
@@ -21,6 +21,53 @@ export const getHeadingsfromTheArticle = () => {
return rows;
};
+export const generatePortalBgColor = (portalColor, theme) => {
+ const baseColor = theme === 'dark' ? 'black' : 'white';
+ return `color-mix(in srgb, ${portalColor} 20%, ${baseColor})`;
+};
+
+export const generatePortalBg = (portalColor, theme) => {
+ const bgImage = theme === 'dark' ? 'hexagon-dark.svg' : 'hexagon-light.svg';
+ return `background: url(/assets/images/hc/${bgImage}) ${generatePortalBgColor(
+ portalColor,
+ theme
+ )}`;
+};
+
+export const generateGradientToBottom = theme => {
+ return `background-image: linear-gradient(to bottom, transparent, ${
+ theme === 'dark' ? '#151718' : 'white'
+ })`;
+};
+
+export const setPortalStyles = theme => {
+ const portalColor = window.portalConfig.portalColor;
+ const portalBgDiv = document.querySelector('#portal-bg');
+ const portalBgGradientDiv = document.querySelector('#portal-bg-gradient');
+
+ if (portalBgDiv) {
+ // Set background for #portal-bg
+ portalBgDiv.setAttribute('style', generatePortalBg(portalColor, theme));
+ }
+
+ if (portalBgGradientDiv) {
+ // Set gradient background for #portal-bg-gradient
+ portalBgGradientDiv.setAttribute('style', generateGradientToBottom(theme));
+ }
+};
+
+export const setPortalClass = theme => {
+ const portalDiv = document.querySelector('#portal');
+ portalDiv.classList.remove('light', 'dark');
+ if (!portalDiv) return;
+ portalDiv.classList.add(theme);
+};
+
+export const updateThemeStyles = theme => {
+ setPortalStyles(theme);
+ setPortalClass(theme);
+};
+
export const InitializationHelpers = {
navigateToLocalePage: () => {
const allLocaleSwitcher = document.querySelector('.locale-switcher');
@@ -36,7 +83,7 @@ export const InitializationHelpers = {
return false;
},
- initalizeSearch: () => {
+ initializeSearch: () => {
const isSearchContainerAvailable = document.querySelector('#search-wrap');
if (isSearchContainerAvailable) {
new Vue({
@@ -51,7 +98,7 @@ export const InitializationHelpers = {
if (isOnArticlePage) {
new Vue({
components: { TableOfContents },
- data: { rows: getHeadingsfromTheArticle() },
+ data: { rows: getHeadingsFromTheArticle() },
template: '',
}).$mount('#cw-hc-toc');
}
@@ -68,13 +115,30 @@ export const InitializationHelpers = {
});
},
+ initializeTheme: () => {
+ const mediaQueryList = window.matchMedia('(prefers-color-scheme: dark)');
+ const getThemePreference = () =>
+ mediaQueryList.matches ? 'dark' : 'light';
+ const themeFromServer = window.portalConfig.theme;
+ if (themeFromServer === 'system') {
+ // Handle dynamic theme changes for system theme
+ mediaQueryList.addEventListener('change', event => {
+ const newTheme = event.matches ? 'dark' : 'light';
+ updateThemeStyles(newTheme);
+ });
+ const themePreference = getThemePreference();
+ updateThemeStyles(themePreference);
+ }
+ },
+
initialize: () => {
if (window.portalConfig.isPlainLayoutEnabled === 'true') {
InitializationHelpers.appendPlainParamToURLs();
} else {
InitializationHelpers.navigateToLocalePage();
- InitializationHelpers.initalizeSearch();
+ InitializationHelpers.initializeSearch();
InitializationHelpers.initializeTableOfContents();
+ // InitializationHelpers.initializeTheme();
}
},
diff --git a/app/javascript/portal/specs/portal.spec.js b/app/javascript/portal/specs/portal.spec.js
index cd4347bad..69e1e81f0 100644
--- a/app/javascript/portal/specs/portal.spec.js
+++ b/app/javascript/portal/specs/portal.spec.js
@@ -1,4 +1,12 @@
-import { InitializationHelpers } from '../portalHelpers';
+import {
+ InitializationHelpers,
+ generatePortalBgColor,
+ generatePortalBg,
+ generateGradientToBottom,
+ setPortalStyles,
+ setPortalClass,
+ updateThemeStyles,
+} from '../portalHelpers';
describe('#navigateToLocalePage', () => {
it('returns correct cookie name', () => {
@@ -21,3 +29,198 @@ describe('#navigateToLocalePage', () => {
);
});
});
+
+describe('Theme Functions', () => {
+ describe('#generatePortalBgColor', () => {
+ it('returns mixed color for dark theme', () => {
+ const result = generatePortalBgColor('#FF5733', 'dark');
+ expect(result).toBe('color-mix(in srgb, #FF5733 20%, black)');
+ });
+
+ it('returns mixed color for light theme', () => {
+ const result = generatePortalBgColor('#FF5733', 'light');
+ expect(result).toBe('color-mix(in srgb, #FF5733 20%, white)');
+ });
+ });
+
+ describe('#generatePortalBg', () => {
+ it('returns background for dark theme', () => {
+ const result = generatePortalBg('#FF5733', 'dark');
+ expect(result).toBe(
+ 'background: url(/assets/images/hc/hexagon-dark.svg) color-mix(in srgb, #FF5733 20%, black)'
+ );
+ });
+
+ it('returns background for light theme', () => {
+ const result = generatePortalBg('#FF5733', 'light');
+ expect(result).toBe(
+ 'background: url(/assets/images/hc/hexagon-light.svg) color-mix(in srgb, #FF5733 20%, white)'
+ );
+ });
+ });
+
+ describe('#generateGradientToBottom', () => {
+ it('returns gradient for dark theme', () => {
+ const result = generateGradientToBottom('dark');
+ expect(result).toBe(
+ 'background-image: linear-gradient(to bottom, transparent, #151718)'
+ );
+ });
+
+ it('returns gradient for light theme', () => {
+ const result = generateGradientToBottom('light');
+ expect(result).toBe(
+ 'background-image: linear-gradient(to bottom, transparent, white)'
+ );
+ });
+ });
+
+ describe('#setPortalStyles', () => {
+ let mockPortalBgDiv;
+ let mockPortalBgGradientDiv;
+
+ beforeEach(() => {
+ // Mocking portal background div
+ mockPortalBgDiv = document.createElement('div');
+ mockPortalBgDiv.id = 'portal-bg';
+ document.body.appendChild(mockPortalBgDiv);
+
+ // Mocking portal background gradient div
+ mockPortalBgGradientDiv = document.createElement('div');
+ mockPortalBgGradientDiv.id = 'portal-bg-gradient';
+ document.body.appendChild(mockPortalBgGradientDiv);
+ });
+
+ afterEach(() => {
+ document.body.innerHTML = '';
+ });
+
+ it('sets styles for portal background based on theme', () => {
+ window.portalConfig = { portalColor: '#FF5733' };
+
+ setPortalStyles('dark');
+ const expectedPortalBgStyle =
+ 'background: url(/assets/images/hc/hexagon-dark.svg) color-mix(in srgb, #FF5733 20%, black)';
+ const expectedGradientStyle =
+ 'background-image: linear-gradient(to bottom, transparent, #151718)';
+
+ expect(mockPortalBgDiv.getAttribute('style')).toBe(expectedPortalBgStyle);
+ expect(mockPortalBgGradientDiv.getAttribute('style')).toBe(
+ expectedGradientStyle
+ );
+ });
+ });
+
+ describe('#setPortalClass', () => {
+ let mockPortalDiv;
+
+ beforeEach(() => {
+ // Mocking portal div
+ mockPortalDiv = document.createElement('div');
+ mockPortalDiv.id = 'portal';
+ mockPortalDiv.classList.add('light');
+ document.body.appendChild(mockPortalDiv);
+ });
+
+ afterEach(() => {
+ document.body.innerHTML = '';
+ });
+
+ it('sets portal class based on theme', () => {
+ setPortalClass('dark');
+
+ expect(mockPortalDiv.classList.contains('dark')).toBe(true);
+ expect(mockPortalDiv.classList.contains('light')).toBe(false);
+ });
+ });
+
+ describe('#updateThemeStyles', () => {
+ let mockPortalDiv;
+ let mockPortalBgDiv;
+ let mockPortalBgGradientDiv;
+
+ beforeEach(() => {
+ // Mocking portal div
+ mockPortalDiv = document.createElement('div');
+ mockPortalDiv.id = 'portal';
+ document.body.appendChild(mockPortalDiv);
+
+ // Mocking portal background div
+ mockPortalBgDiv = document.createElement('div');
+ mockPortalBgDiv.id = 'portal-bg';
+ document.body.appendChild(mockPortalBgDiv);
+
+ // Mocking portal background gradient div
+ mockPortalBgGradientDiv = document.createElement('div');
+ mockPortalBgGradientDiv.id = 'portal-bg-gradient';
+ document.body.appendChild(mockPortalBgGradientDiv);
+ });
+
+ afterEach(() => {
+ document.body.innerHTML = '';
+ });
+
+ it('updates theme styles based on theme', () => {
+ window.portalConfig = { portalColor: '#FF5733' };
+
+ updateThemeStyles('dark');
+
+ const expectedPortalBgStyle =
+ 'background: url(/assets/images/hc/hexagon-dark.svg) color-mix(in srgb, #FF5733 20%, black)';
+ const expectedGradientStyle =
+ 'background-image: linear-gradient(to bottom, transparent, #151718)';
+
+ expect(mockPortalDiv.classList.contains('dark')).toBe(true);
+ expect(mockPortalBgDiv.getAttribute('style')).toBe(expectedPortalBgStyle);
+ expect(mockPortalBgGradientDiv.getAttribute('style')).toBe(
+ expectedGradientStyle
+ );
+ });
+ });
+
+ describe('#initializeTheme', () => {
+ let mockPortalDiv;
+
+ beforeEach(() => {
+ mockPortalDiv = document.createElement('div');
+ mockPortalDiv.id = 'portal';
+ document.body.appendChild(mockPortalDiv);
+ });
+
+ afterEach(() => {
+ document.body.innerHTML = '';
+ });
+
+ it('updates theme based on system preferences', () => {
+ const mediaQueryList = {
+ matches: true,
+ addEventListener: jest.fn(),
+ };
+ window.matchMedia = jest.fn().mockReturnValue(mediaQueryList);
+ window.portalConfig = { theme: 'system' };
+
+ InitializationHelpers.initializeTheme();
+
+ expect(mediaQueryList.addEventListener).toBeCalledWith(
+ 'change',
+ expect.any(Function)
+ );
+ expect(mockPortalDiv.classList.contains('dark')).toBe(true);
+ });
+
+ it('does not update theme if themeFromServer is not "system"', () => {
+ const mediaQueryList = {
+ matches: true,
+ addEventListener: jest.fn(),
+ };
+ window.matchMedia = jest.fn().mockReturnValue(mediaQueryList);
+ window.portalConfig = { theme: 'dark' };
+
+ InitializationHelpers.initializeTheme();
+
+ expect(mediaQueryList.addEventListener).not.toBeCalled();
+ expect(mockPortalDiv.classList.contains('dark')).toBe(false);
+ expect(mockPortalDiv.classList.contains('light')).toBe(false);
+ });
+ });
+});
diff --git a/app/views/layouts/portal.html.erb b/app/views/layouts/portal.html.erb
index d446e2e6f..2ae188fc3 100644
--- a/app/views/layouts/portal.html.erb
+++ b/app/views/layouts/portal.html.erb
@@ -48,6 +48,8 @@ By default, it renders: