diff --git a/special-pages/pages/history/app/components/App.jsx b/special-pages/pages/history/app/components/App.jsx
index be4801e389..10f172717c 100644
--- a/special-pages/pages/history/app/components/App.jsx
+++ b/special-pages/pages/history/app/components/App.jsx
@@ -1,7 +1,6 @@
import { h } from 'preact';
import cn from 'classnames';
import styles from './App.module.css';
-import { useEnv } from '../../../../shared/components/EnvironmentProvider.js';
import { Header } from './Header.js';
import { ResultsContainer } from './Results.js';
import { useEffect, useRef } from 'preact/hooks';
@@ -20,11 +19,12 @@ import { useRangesData } from '../global/Providers/HistoryServiceProvider.js';
import { usePlatformName } from '../types.js';
import { useLayoutMode } from '../global/hooks/useLayoutMode.js';
import { useClickAnywhereElse } from '../global/hooks/useClickAnywhereElse.jsx';
+import { useTheme } from '../global/Providers/ThemeProvider.js';
export function App() {
const platformName = usePlatformName();
const mainRef = useRef(/** @type {HTMLElement|null} */ (null));
- const { isDarkMode } = useEnv();
+ const { theme, themeVariant } = useTheme();
const ranges = useRangesData();
const query = useQueryContext();
const mode = useLayoutMode();
@@ -66,7 +66,8 @@ export function App() {
return (
{
+ const unsubscribe = history.messaging.subscribe('onThemeUpdate', (data) => {
+ setExplicitTheme(data.theme);
+ setExplicitThemeVariant(data.themeVariant);
+ });
+ return unsubscribe;
+ }, [history]);
+
+ // Derive theme from explicit updates, initial theme, or system preference (in that order)
+ const theme = explicitTheme ?? initialTheme ?? (isDarkMode ? 'dark' : 'light');
+ const themeVariant = explicitThemeVariant ?? initialThemeVariant ?? 'default';
+
+ return
{children};
+}
+
+export function useTheme() {
+ return useContext(ThemeContext);
+}
diff --git a/special-pages/pages/history/app/index.js b/special-pages/pages/history/app/index.js
index edb9c382f3..26ccbfe57d 100644
--- a/special-pages/pages/history/app/index.js
+++ b/special-pages/pages/history/app/index.js
@@ -15,6 +15,7 @@ import { Settings } from './Settings.js';
import { SelectionProvider } from './global/Providers/SelectionProvider.js';
import { QueryProvider } from './global/Providers/QueryProvider.js';
import { InlineErrorBoundary } from '../../../shared/components/InlineErrorBoundary.js';
+import { ThemeProvider } from './global/Providers/ThemeProvider.js';
/**
* @param {Element} root
@@ -84,15 +85,17 @@ export async function init(root, messaging, baseEnvironment) {
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
@@ -117,6 +120,9 @@ export async function init(root, messaging, baseEnvironment) {
* @param {import("../types/history.ts").DefaultStyles | null | undefined} defaultStyles
*/
function applyDefaultStyles(defaultStyles) {
+ if (defaultStyles?.lightBackgroundColor || defaultStyles?.darkBackgroundColor) {
+ console.warn('defaultStyles is deprecated. Use themeVariant instead. This will override theme variant colors.', defaultStyles);
+ }
if (defaultStyles?.lightBackgroundColor) {
document.body.style.setProperty('--default-light-background-color', defaultStyles.lightBackgroundColor);
}
diff --git a/special-pages/pages/history/app/mocks/mock-transport.js b/special-pages/pages/history/app/mocks/mock-transport.js
index d7b4d2948e..41199c7f44 100644
--- a/special-pages/pages/history/app/mocks/mock-transport.js
+++ b/special-pages/pages/history/app/mocks/mock-transport.js
@@ -163,6 +163,23 @@ export function mockTransport() {
},
};
+ // Allow theme override via URL params
+ if (url.searchParams.has('theme')) {
+ const value = url.searchParams.get('theme');
+ if (value === 'light' || value === 'dark') {
+ initial.theme = /** @type {import('../../types/history.ts').BrowserTheme} */ (value);
+ }
+ }
+
+ // Allow themeVariant override via URL params
+ if (url.searchParams.has('themeVariant')) {
+ const value = url.searchParams.get('themeVariant');
+ const validVariants = ['default', 'coolGray', 'slateBlue', 'green', 'violet', 'rose', 'orange', 'desert'];
+ if (value && validVariants.includes(value)) {
+ initial.themeVariant = /** @type {import('../../types/history.ts').ThemeVariant} */ (value);
+ }
+ }
+
return Promise.resolve(initial);
}
diff --git a/special-pages/pages/history/integration-tests/history-theme.spec.js b/special-pages/pages/history/integration-tests/history-theme.spec.js
new file mode 100644
index 0000000000..d03315f5b6
--- /dev/null
+++ b/special-pages/pages/history/integration-tests/history-theme.spec.js
@@ -0,0 +1,53 @@
+import { test } from '@playwright/test';
+import { HistoryTestPage } from './history.page.js';
+
+test.describe('history theme and theme variants', () => {
+ test('setting theme = dark and themeVariant via initialSetup', async ({ page }, workerInfo) => {
+ const hp = HistoryTestPage.create(page, workerInfo);
+ await hp.openPage({ additional: { theme: 'dark', themeVariant: 'violet' } });
+ await hp.hasTheme('dark', 'violet');
+ await hp.hasBackgroundColor({ hex: '#2e2158' });
+ });
+
+ test('setting theme = light and themeVariant via initialSetup', async ({ page }, workerInfo) => {
+ const hp = HistoryTestPage.create(page, workerInfo);
+ await hp.openPage({ additional: { theme: 'light', themeVariant: 'coolGray' } });
+ await hp.hasTheme('light', 'coolGray');
+ await hp.hasBackgroundColor({ hex: '#d2d5e3' });
+ });
+
+ test('light theme and default themeVariant when unspecified', async ({ page }, workerInfo) => {
+ const hp = HistoryTestPage.create(page, workerInfo);
+ await hp.openPage();
+ await hp.hasTheme('light', 'default');
+ await hp.hasBackgroundColor({ hex: '#fafafa' });
+ });
+
+ test('dark theme and default themeVariant when unspecified', async ({ page }, workerInfo) => {
+ const hp = HistoryTestPage.create(page, workerInfo);
+ await hp.darkMode();
+ await hp.openPage();
+ await hp.hasTheme('dark', 'default');
+ await hp.hasBackgroundColor({ hex: '#333333' });
+ });
+
+ test('changing theme to dark and themeVariant using onThemeUpdate', async ({ page }, workerInfo) => {
+ const hp = HistoryTestPage.create(page, workerInfo);
+ await hp.openPage({ additional: { theme: 'light', themeVariant: 'desert' } });
+ await hp.hasTheme('light', 'desert');
+ await hp.hasBackgroundColor({ hex: '#eee9e1' });
+ await hp.acceptsThemeUpdate('dark', 'slateBlue');
+ await hp.hasTheme('dark', 'slateBlue');
+ await hp.hasBackgroundColor({ hex: '#1e3347' });
+ });
+
+ test('changing theme to light and themeVariant using onThemeUpdate', async ({ page }, workerInfo) => {
+ const hp = HistoryTestPage.create(page, workerInfo);
+ await hp.openPage({ additional: { theme: 'dark', themeVariant: 'rose' } });
+ await hp.hasTheme('dark', 'rose');
+ await hp.hasBackgroundColor({ hex: '#5b194b' });
+ await hp.acceptsThemeUpdate('light', 'green');
+ await hp.hasTheme('light', 'green');
+ await hp.hasBackgroundColor({ hex: '#e3eee1' });
+ });
+});
diff --git a/special-pages/pages/history/integration-tests/history.page.js b/special-pages/pages/history/integration-tests/history.page.js
index 3e12d6fd1c..e0eeaafb4a 100644
--- a/special-pages/pages/history/integration-tests/history.page.js
+++ b/special-pages/pages/history/integration-tests/history.page.js
@@ -610,6 +610,23 @@ export class HistoryTestPage {
});
expect(borderTopColor).toBe(rgb);
}
+
+ /**
+ * @param {import('../types/history.ts').BrowserTheme} theme
+ * @param {import('../types/history.ts').ThemeVariant} themeVariant
+ */
+ async acceptsThemeUpdate(theme, themeVariant) {
+ await this.mocks.simulateSubscriptionMessage('onThemeUpdate', { theme, themeVariant });
+ }
+
+ /**
+ * @param {import('../types/history.ts').BrowserTheme} theme
+ * @param {import('../types/history.ts').ThemeVariant} themeVariant
+ */
+ async hasTheme(theme, themeVariant) {
+ await expect(this.page.locator('[data-layout-mode]')).toHaveAttribute('data-theme', theme);
+ await expect(this.page.locator('[data-layout-mode]')).toHaveAttribute('data-theme-variant', themeVariant);
+ }
}
/**
diff --git a/special-pages/pages/history/messages/initialSetup.response.json b/special-pages/pages/history/messages/initialSetup.response.json
index 7ae6a89333..0a8d359b45 100644
--- a/special-pages/pages/history/messages/initialSetup.response.json
+++ b/special-pages/pages/history/messages/initialSetup.response.json
@@ -20,10 +20,17 @@
}
}
},
+ "theme": {
+ "$ref": "./types/browser-theme.json"
+ },
+ "themeVariant": {
+ "$ref": "./types/theme-variant.json"
+ },
"customizer": {
"type": "object",
"properties": {
"defaultStyles": {
+ "deprecated": true,
"oneOf": [
{
"type": "null"
diff --git a/special-pages/pages/history/messages/onThemeUpdate.subscribe.json b/special-pages/pages/history/messages/onThemeUpdate.subscribe.json
new file mode 100644
index 0000000000..8af166e6f3
--- /dev/null
+++ b/special-pages/pages/history/messages/onThemeUpdate.subscribe.json
@@ -0,0 +1,13 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "required": ["theme", "themeVariant"],
+ "properties": {
+ "theme": {
+ "$ref": "./types/browser-theme.json"
+ },
+ "themeVariant": {
+ "$ref": "./types/theme-variant.json"
+ }
+ }
+}
diff --git a/special-pages/pages/history/messages/types/browser-theme.json b/special-pages/pages/history/messages/types/browser-theme.json
new file mode 100644
index 0000000000..68f0ecc065
--- /dev/null
+++ b/special-pages/pages/history/messages/types/browser-theme.json
@@ -0,0 +1,8 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "title": "Browser Theme",
+ "enum": [
+ "light",
+ "dark"
+ ]
+}
diff --git a/special-pages/pages/history/messages/types/theme-variant.json b/special-pages/pages/history/messages/types/theme-variant.json
new file mode 100644
index 0000000000..c66dee27e4
--- /dev/null
+++ b/special-pages/pages/history/messages/types/theme-variant.json
@@ -0,0 +1,14 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "title": "Theme Variant",
+ "enum": [
+ "default",
+ "coolGray",
+ "slateBlue",
+ "green",
+ "violet",
+ "rose",
+ "orange",
+ "desert"
+ ]
+}
diff --git a/special-pages/pages/history/styles/history-theme.css b/special-pages/pages/history/styles/history-theme.css
index 05ab00bc15..db0208c11a 100644
--- a/special-pages/pages/history/styles/history-theme.css
+++ b/special-pages/pages/history/styles/history-theme.css
@@ -68,3 +68,40 @@ body {
--history-text-invert: var(--color-black-at-84);
--history-text-muted: var(--color-white-at-60);
}
+
+/* TODO: Use colour variables from design-tokens */
+
+[data-theme-variant="coolGray"] {
+ --default-light-background-color: #d2d5e3;
+ --default-dark-background-color: #2b2f45;
+}
+
+[data-theme-variant="slateBlue"] {
+ --default-light-background-color: #d2e5f3;
+ --default-dark-background-color: #1e3347;
+}
+
+[data-theme-variant="green"] {
+ --default-light-background-color: #e3eee1;
+ --default-dark-background-color: #203b30;
+}
+
+[data-theme-variant="violet"] {
+ --default-light-background-color: #e7e4f5;
+ --default-dark-background-color: #2e2158;
+}
+
+[data-theme-variant="rose"] {
+ --default-light-background-color: #f8ebf5;
+ --default-dark-background-color: #5b194b;
+}
+
+[data-theme-variant="orange"] {
+ --default-light-background-color: #fcedd8;
+ --default-dark-background-color: #54240c;
+}
+
+[data-theme-variant="desert"] {
+ --default-light-background-color: #eee9e1;
+ --default-dark-background-color: #3c3833;
+}
diff --git a/special-pages/pages/history/types/history.ts b/special-pages/pages/history/types/history.ts
index 967558d6bf..447fbecab3 100644
--- a/special-pages/pages/history/types/history.ts
+++ b/special-pages/pages/history/types/history.ts
@@ -33,6 +33,8 @@ export type RangeId =
| "sunday"
| "older"
| "sites";
+export type BrowserTheme = "light" | "dark";
+export type ThemeVariant = "default" | "coolGray" | "slateBlue" | "green" | "violet" | "rose" | "orange" | "desert";
export type QueryKind = SearchTerm | DomainFilter | RangeFilter;
/**
* Indicates the query was triggered before the UI was rendered
@@ -65,6 +67,7 @@ export interface HistoryMessages {
| GetRangesRequest
| InitialSetupRequest
| QueryRequest;
+ subscriptions: OnThemeUpdateSubscription;
}
/**
* Generated from @see "../messages/open.notify.json"
@@ -197,7 +200,12 @@ export interface InitialSetupResponse {
platform: {
name: "macos" | "windows" | "android" | "ios" | "integration";
};
+ theme?: BrowserTheme;
+ themeVariant?: ThemeVariant;
customizer?: {
+ /**
+ * @deprecated
+ */
defaultStyles?: null | DefaultStyles;
};
}
@@ -286,10 +294,22 @@ export interface HistoryItem {
url: string;
favicon?: Favicon;
}
+/**
+ * Generated from @see "../messages/onThemeUpdate.subscribe.json"
+ */
+export interface OnThemeUpdateSubscription {
+ subscriptionEvent: "onThemeUpdate";
+ params: OnThemeUpdateSubscribe;
+}
+export interface OnThemeUpdateSubscribe {
+ theme: BrowserTheme;
+ themeVariant: ThemeVariant;
+}
declare module "../src/index.js" {
export interface HistoryPage {
notify: import("@duckduckgo/messaging/lib/shared-types").MessagingBase
['notify'],
- request: import("@duckduckgo/messaging/lib/shared-types").MessagingBase['request']
+ request: import("@duckduckgo/messaging/lib/shared-types").MessagingBase['request'],
+ subscribe: import("@duckduckgo/messaging/lib/shared-types").MessagingBase['subscribe']
}
}
\ No newline at end of file
diff --git a/special-pages/playwright.config.js b/special-pages/playwright.config.js
index 5da7537260..96ba3a15c8 100644
--- a/special-pages/playwright.config.js
+++ b/special-pages/playwright.config.js
@@ -35,6 +35,7 @@ export default defineConfig({
'activity.spec.js',
'history.spec.js',
'history-selections.spec.js',
+ 'history-theme.spec.js',
'history.screenshots.spec.js',
'protections.spec.js',
'protections.screenshots.spec.js',