Skip to content

Commit 003b9c7

Browse files
Special Error: Stub out support for theming (#2076)
* Add theme and variant support to SpecialError page * Refactor: Update comment for theme variant background colors Co-authored-by: randerson <[email protected]> * Apply suggestion from @noisysocks * Fix ?display=components not picking up theming --------- Co-authored-by: Cursor Agent <[email protected]>
1 parent e745ace commit 003b9c7

File tree

15 files changed

+320
-32
lines changed

15 files changed

+320
-32
lines changed

special-pages/pages/special-error/app/components/App.jsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ function PageTitle() {
4848

4949
export function App() {
5050
const { messaging } = useMessaging();
51-
const { isDarkMode } = useEnv();
5251

5352
/**
5453
* @param {Error} error
@@ -60,7 +59,7 @@ export function App() {
6059
}
6160

6261
return (
63-
<main className={styles.main} data-theme={isDarkMode ? 'dark' : 'light'}>
62+
<main className={styles.main}>
6463
<PageTitle />
6564
<ErrorBoundary didCatch={({ error }) => didCatch(error)} fallback={<ErrorFallback />}>
6665
<SpecialErrorView />

special-pages/pages/special-error/app/index.js

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Components } from './components/Components.jsx';
77
import enStrings from '../public/locales/en/special-error.json';
88
import { TranslationProvider } from '../../../shared/components/TranslationsProvider.js';
99
import { MessagingProvider } from './providers/MessagingProvider.js';
10+
import { ThemeProvider } from './providers/ThemeProvider.js';
1011
import { SettingsProvider } from './providers/SettingsProvider.jsx';
1112
import { SpecialErrorProvider } from './providers/SpecialErrorProvider.js';
1213
import { callWithRetry } from '../../../shared/call-with-retry.js';
@@ -65,11 +66,13 @@ export async function init(messaging, baseEnvironment) {
6566
<UpdateEnvironment search={window.location.search} />
6667
<TranslationProvider translationObject={strings} fallback={enStrings} textLength={environment.textLength}>
6768
<MessagingProvider messaging={messaging}>
68-
<SettingsProvider settings={settings}>
69-
<SpecialErrorProvider specialError={specialError}>
70-
<App />
71-
</SpecialErrorProvider>
72-
</SettingsProvider>
69+
<ThemeProvider initialTheme={init.theme} initialThemeVariant={init.themeVariant}>
70+
<SettingsProvider settings={settings}>
71+
<SpecialErrorProvider specialError={specialError}>
72+
<App />
73+
</SpecialErrorProvider>
74+
</SettingsProvider>
75+
</ThemeProvider>
7376
</MessagingProvider>
7477
</TranslationProvider>
7578
</EnvironmentProvider>,
@@ -79,11 +82,13 @@ export async function init(messaging, baseEnvironment) {
7982
render(
8083
<EnvironmentProvider debugState={false} injectName={environment.injectName}>
8184
<TranslationProvider translationObject={strings} fallback={enStrings} textLength={environment.textLength}>
82-
<SettingsProvider settings={settings}>
83-
<SpecialErrorProvider specialError={specialError}>
84-
<Components />
85-
</SpecialErrorProvider>
86-
</SettingsProvider>
85+
<ThemeProvider initialTheme={init.theme} initialThemeVariant={init.themeVariant}>
86+
<SettingsProvider settings={settings}>
87+
<SpecialErrorProvider specialError={specialError}>
88+
<Components />
89+
</SpecialErrorProvider>
90+
</SettingsProvider>
91+
</ThemeProvider>
8792
</TranslationProvider>
8893
</EnvironmentProvider>,
8994
root,
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { createContext, h } from 'preact';
2+
import { useContext, useEffect, useState } from 'preact/hooks';
3+
import { useMessaging } from './MessagingProvider.js';
4+
import { useEnv } from '../../../../shared/components/EnvironmentProvider.js';
5+
6+
/**
7+
* @typedef {import('../../types/special-error').BrowserTheme} BrowserTheme
8+
* @typedef {import('../../types/special-error').ThemeVariant} ThemeVariant
9+
*/
10+
11+
const ThemeContext = createContext({
12+
/** @type {BrowserTheme} */
13+
theme: 'light',
14+
/** @type {ThemeVariant} */
15+
themeVariant: 'default',
16+
});
17+
18+
/**
19+
* @param {object} props
20+
* @param {import('preact').ComponentChild} props.children
21+
* @param {BrowserTheme | undefined} props.initialTheme
22+
* @param {ThemeVariant | undefined} props.initialThemeVariant
23+
*/
24+
export function ThemeProvider({ children, initialTheme, initialThemeVariant }) {
25+
const { isDarkMode } = useEnv();
26+
const { messaging } = useMessaging();
27+
28+
// Track explicit theme updates from onThemeUpdate subscription
29+
const [explicitTheme, setExplicitTheme] = useState(/** @type {BrowserTheme | undefined} */ (undefined));
30+
const [explicitThemeVariant, setExplicitThemeVariant] = useState(/** @type {ThemeVariant | undefined} */ (undefined));
31+
32+
useEffect(() => {
33+
if (!messaging) return;
34+
const unsubscribe = messaging.onThemeUpdate((data) => {
35+
setExplicitTheme(data.theme);
36+
setExplicitThemeVariant(data.themeVariant);
37+
});
38+
return unsubscribe;
39+
}, [messaging]);
40+
41+
// Derive theme from explicit updates, initial theme, or system preference (in that order)
42+
const theme = explicitTheme ?? initialTheme ?? (isDarkMode ? 'dark' : 'light');
43+
const themeVariant = explicitThemeVariant ?? initialThemeVariant ?? 'default';
44+
45+
// Sync theme attributes to <body>
46+
useEffect(() => {
47+
document.body.dataset.theme = theme;
48+
}, [theme]);
49+
useEffect(() => {
50+
document.body.dataset.themeVariant = themeVariant;
51+
}, [themeVariant]);
52+
53+
return <ThemeContext.Provider value={{ theme, themeVariant }}>{children}</ThemeContext.Provider>;
54+
}
55+
56+
export function useTheme() {
57+
return useContext(ThemeContext);
58+
}
Lines changed: 52 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
:root {
2-
/* Light theme colors */
3-
--theme-background-color: var(--color-gray-20);
2+
--default-light-background-color: var(--color-gray-20);
3+
--default-dark-background-color: var(--color-gray-85);
4+
}
5+
6+
body[data-theme='light'] {
7+
--theme-background-color: var(--default-light-background-color);
48
--theme-text-primary-color: var(--color-black-at-84);
59
--link-color: var(--color-black);
610
--border-color: rgba(0, 0, 0, 0.1);
@@ -11,23 +15,55 @@
1115
--visit-site-color: var(--color-black);
1216
}
1317

14-
@media (prefers-color-scheme: dark) {
15-
:root {
16-
/* Dark theme colors */
17-
--theme-background-color: var(--color-gray-85);
18-
--theme-text-primary-color: var(--color-white-at-84);
19-
--link-color: var(--color-gray-40);
20-
--border-color: var(--color-white-at-18);
18+
body[data-theme='dark'] {
19+
--theme-background-color: var(--default-dark-background-color);
20+
--theme-text-primary-color: var(--color-white-at-84);
21+
--link-color: var(--color-gray-40);
22+
--border-color: var(--color-white-at-18);
23+
24+
--container-bg: var(--color-gray-90);
25+
--advanced-info-bg: #2f2f2f;
26+
27+
--visit-site-color: var(--color-gray-40);
28+
}
29+
30+
body[data-theme='dark'][data-platform-name='ios'][data-theme-variant='default'] {
31+
--theme-background-color: #222;
32+
}
33+
34+
/* TODO: Use colour variables from design-tokens */
2135

22-
--container-bg: var(--color-gray-90);
23-
--advanced-info-bg: #2f2f2f;
36+
body[data-theme-variant='coolGray'] {
37+
--default-light-background-color: #d2d5e3;
38+
--default-dark-background-color: #2b2f45;
39+
}
40+
41+
body[data-theme-variant='slateBlue'] {
42+
--default-light-background-color: #d2e5f3;
43+
--default-dark-background-color: #1e3347;
44+
}
45+
46+
body[data-theme-variant='green'] {
47+
--default-light-background-color: #e3eee1;
48+
--default-dark-background-color: #203b30;
49+
}
2450

25-
--visit-site-color: var(--color-gray-40);
26-
}
51+
body[data-theme-variant='violet'] {
52+
--default-light-background-color: #e7e4f5;
53+
--default-dark-background-color: #2e2158;
54+
}
2755

28-
[data-platform-name="ios"] {
29-
--theme-background-color: #222;
30-
}
56+
body[data-theme-variant='rose'] {
57+
--default-light-background-color: #f8ebf5;
58+
--default-dark-background-color: #5b194b;
3159
}
3260

61+
body[data-theme-variant='orange'] {
62+
--default-light-background-color: #fcedd8;
63+
--default-dark-background-color: #54240c;
64+
}
3365

66+
body[data-theme-variant='desert'] {
67+
--default-light-background-color: #eee9e1;
68+
--default-dark-background-color: #3c3833;
69+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { test } from '@playwright/test';
2+
import { SpecialErrorPage } from './special-error.js';
3+
4+
test.describe('special-error theme and theme variants', () => {
5+
test('setting theme = dark and themeVariant via initialSetup', async ({ page }, workerInfo) => {
6+
const sp = SpecialErrorPage.create(page, workerInfo);
7+
await sp.openPage({ additional: { theme: 'dark', themeVariant: 'violet' } });
8+
await sp.hasTheme('dark', 'violet');
9+
await sp.hasBackgroundColor({ hex: '#2e2158' });
10+
});
11+
12+
test('setting theme = light and themeVariant via initialSetup', async ({ page }, workerInfo) => {
13+
const sp = SpecialErrorPage.create(page, workerInfo);
14+
await sp.openPage({ additional: { theme: 'light', themeVariant: 'coolGray' } });
15+
await sp.hasTheme('light', 'coolGray');
16+
await sp.hasBackgroundColor({ hex: '#d2d5e3' });
17+
});
18+
19+
test('light theme and default themeVariant when unspecified', async ({ page }, workerInfo) => {
20+
const sp = SpecialErrorPage.create(page, workerInfo);
21+
await sp.openPage();
22+
await sp.hasTheme('light', 'default');
23+
await sp.hasBackgroundColor({ hex: '#eeeeee' });
24+
});
25+
26+
test('dark theme and default themeVariant when unspecified', async ({ page }, workerInfo) => {
27+
const sp = SpecialErrorPage.create(page, workerInfo);
28+
await sp.darkMode();
29+
await sp.openPage();
30+
await sp.hasTheme('dark', 'default');
31+
const isIOS = sp.platform.name === 'ios'; // iOS has a different default background color
32+
await sp.hasBackgroundColor({ hex: isIOS ? '#222222' : '#333333' });
33+
});
34+
35+
test('changing theme to dark and themeVariant using onThemeUpdate', async ({ page }, workerInfo) => {
36+
const sp = SpecialErrorPage.create(page, workerInfo);
37+
await sp.openPage({ additional: { theme: 'light', themeVariant: 'desert' } });
38+
await sp.hasTheme('light', 'desert');
39+
await sp.hasBackgroundColor({ hex: '#eee9e1' });
40+
await sp.acceptsThemeUpdate('dark', 'slateBlue');
41+
await sp.hasTheme('dark', 'slateBlue');
42+
await sp.hasBackgroundColor({ hex: '#1e3347' });
43+
});
44+
45+
test('changing theme to light and themeVariant using onThemeUpdate', async ({ page }, workerInfo) => {
46+
const sp = SpecialErrorPage.create(page, workerInfo);
47+
await sp.openPage({ additional: { theme: 'dark', themeVariant: 'rose' } });
48+
await sp.hasTheme('dark', 'rose');
49+
await sp.hasBackgroundColor({ hex: '#5b194b' });
50+
await sp.acceptsThemeUpdate('light', 'green');
51+
await sp.hasTheme('light', 'green');
52+
await sp.hasBackgroundColor({ hex: '#e3eee1' });
53+
});
54+
});

special-pages/pages/special-error/integration-tests/special-error.js

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,16 @@ export class SpecialErrorPage {
4040
* @param {keyof sampleData} [params.errorId] - ID of the error to be mocked (see sampleData.js)
4141
* @param {PlatformInfo['name']} [params.platformName] - platform name
4242
* @param {string} [params.locale] - locale
43+
* @param {Record<string, any>} [params.additional] - Optional map of key/values to add to initialSetup
4344
*/
44-
async openPage({ env = 'app', willThrow = false, errorId = 'ssl.expired', platformName = this.platform.name, locale } = {}) {
45+
async openPage({
46+
env = 'app',
47+
willThrow = false,
48+
errorId = 'ssl.expired',
49+
platformName = this.platform.name,
50+
locale,
51+
additional,
52+
} = {}) {
4553
if (platformName === 'extension') {
4654
throw new Error(`Unsupported platform ${platformName}`);
4755
}
@@ -67,6 +75,10 @@ export class SpecialErrorPage {
6775
initialSetup.locale = locale;
6876
}
6977

78+
if (additional) {
79+
Object.assign(initialSetup, additional);
80+
}
81+
7082
this.mocks.defaultResponses({
7183
initialSetup,
7284
});
@@ -396,4 +408,34 @@ export class SpecialErrorPage {
396408
}),
397409
);
398410
}
411+
412+
/**
413+
* @param {object} params
414+
* @param {string} params.hex
415+
* @returns {Promise<void>}
416+
*/
417+
async hasBackgroundColor({ hex }) {
418+
const r = parseInt(hex.slice(1, 3), 16);
419+
const g = parseInt(hex.slice(3, 5), 16);
420+
const b = parseInt(hex.slice(5, 7), 16);
421+
const rgb = `rgb(${[r, g, b].join(', ')})`;
422+
await expect(this.page.locator('body')).toHaveCSS('background-color', rgb, { timeout: 50 });
423+
}
424+
425+
/**
426+
* @param {import('../types/special-error.ts').BrowserTheme} theme
427+
* @param {import('../types/special-error.ts').ThemeVariant} themeVariant
428+
*/
429+
async acceptsThemeUpdate(theme, themeVariant) {
430+
await this.mocks.simulateSubscriptionMessage('onThemeUpdate', { theme, themeVariant });
431+
}
432+
433+
/**
434+
* @param {import('../types/special-error.ts').BrowserTheme} theme
435+
* @param {import('../types/special-error.ts').ThemeVariant} themeVariant
436+
*/
437+
async hasTheme(theme, themeVariant) {
438+
await expect(this.page.locator('body')).toHaveAttribute('data-theme', theme);
439+
await expect(this.page.locator('body')).toHaveAttribute('data-theme-variant', themeVariant);
440+
}
399441
}

special-pages/pages/special-error/messages/initialSetup.response.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,12 @@
113113
"localeStrings": {
114114
"type": "string",
115115
"description": "Optional locale-specific strings"
116+
},
117+
"theme": {
118+
"$ref": "./types/browser-theme.json"
119+
},
120+
"themeVariant": {
121+
"$ref": "./types/theme-variant.json"
116122
}
117123
}
118124
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"type": "object",
4+
"required": ["theme", "themeVariant"],
5+
"properties": {
6+
"theme": {
7+
"$ref": "./types/browser-theme.json"
8+
},
9+
"themeVariant": {
10+
"$ref": "./types/theme-variant.json"
11+
}
12+
}
13+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"title": "Browser Theme",
4+
"enum": [
5+
"light",
6+
"dark"
7+
]
8+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"title": "Theme Variant",
4+
"enum": [
5+
"default",
6+
"coolGray",
7+
"slateBlue",
8+
"green",
9+
"violet",
10+
"rose",
11+
"orange",
12+
"desert"
13+
]
14+
}

0 commit comments

Comments
 (0)