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',