Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions locales/en-US/app.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ AppHeader--app-header = <header>{ -profiler-brand-name }</header> — <subheader
AppHeader--github-icon =
.title = Go to our Git repository (this opens in a new window)

## ThemeToggle
## They are used at the top right side of the home page to switch between themes.

ThemeToggle--system =
.title = Follow system theme preference
ThemeToggle--light =
.title = Use light theme
ThemeToggle--dark =
.title = Use dark theme

## AppViewRouter
## This is used for displaying errors when loading the application.

Expand Down
4 changes: 4 additions & 0 deletions res/img/svg/theme-dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions res/img/svg/theme-light.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions res/img/svg/theme-system.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 9 additions & 4 deletions src/components/app/AppHeader.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
--internal-border-color: #ddd;

display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 0.5em;
border-bottom: 1px solid var(--internal-border-color);
Expand Down Expand Up @@ -41,15 +42,19 @@
font-weight: normal;
}

.appHeaderRightControls {
display: flex;
align-items: center;
gap: 0.5em;
}

.appHeaderGithubIcon {
display: inline-flex;
align-items: center;
margin: 0 0.2em;
transition: opacity 250ms var(--animation-curve);
}

.appHeaderGithubIcon svg {
vertical-align: middle;
}

.appHeaderGithubIcon:hover {
opacity: 0.5;
}
Expand Down
48 changes: 26 additions & 22 deletions src/components/app/AppHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import * as React from 'react';

import { InnerNavigationLink } from 'firefox-profiler/components/shared/InnerNavigationLink';
import { ThemeToggle } from './ThemeToggle';

import './AppHeader.css';
import { Localized } from '@fluent/react';
Expand Down Expand Up @@ -42,29 +43,32 @@ export class AppHeader extends React.PureComponent<{}> {
</span>
</span>
</Localized>
<Localized id="AppHeader--github-icon" attrs={{ title: true }}>
<a
className="appHeaderGithubIcon"
href="https://github.com/firefox-devtools/profiler"
target="_blank"
rel="noopener noreferrer"
title="Go to our Git repository (this opens in a new window)"
>
<svg
width="22"
height="22"
className="octicon octicon-mark-github"
viewBox="0 0 16 16"
version="1.1"
aria-label="github"
<div className="appHeaderRightControls">
<ThemeToggle />
<Localized id="AppHeader--github-icon" attrs={{ title: true }}>
<a
className="appHeaderGithubIcon"
href="https://github.com/firefox-devtools/profiler"
target="_blank"
rel="noopener noreferrer"
title="Go to our Git repository (this opens in a new window)"
>
<path
fillRule="evenodd"
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"
/>
</svg>
</a>
</Localized>
<svg
width="22"
height="22"
className="octicon octicon-mark-github"
viewBox="0 0 16 16"
version="1.1"
aria-label="github"
>
<path
fillRule="evenodd"
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"
/>
</svg>
</a>
</Localized>
</div>
</h1>
</header>
);
Expand Down
101 changes: 101 additions & 0 deletions src/components/app/ThemeToggle.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

.themeToggle {
display: inline-flex;
align-items: center;
padding: 2px;
border: 1px solid var(--grey-30);
border-radius: 6px;
margin: 0 0.5em;
background: var(--grey-10);
box-shadow: 0 1px 2px rgb(0 0 0 / 0.05);
}

.themeToggleButton {
position: relative;
display: flex;
width: 32px;
height: 28px;
align-items: center;
justify-content: center;
padding: 0;
border: none;
border-radius: 4px;
background: transparent;
color: var(--grey-50);
cursor: pointer;
font-size: 16px;
transition: all 150ms ease-in-out;
}

.themeToggleButton:hover:not(.themeToggleButton-active) {
background: var(--grey-20);
}

.themeToggleButton-active {
background: white;
box-shadow:
0 1px 3px rgb(0 0 0 / 0.12),
0 1px 2px rgb(0 0 0 / 0.08);
color: var(--grey-90);
}

.themeToggleButton-active:hover {
background: white;
}

/* Theme toggle icons using CSS mask */

.themeToggleIcon {
display: block;
width: 16px;
height: 16px;
background-color: currentcolor;
mask-position: center;
mask-repeat: no-repeat;
mask-size: contain;
}

.themeToggleIcon--light {
mask-image: url('firefox-profiler-res/img/svg/theme-light.svg');
}

.themeToggleIcon--system {
mask-image: url('firefox-profiler-res/img/svg/theme-system.svg');
}

.themeToggleIcon--dark {
mask-image: url('firefox-profiler-res/img/svg/theme-dark.svg');
}

/* Dark mode styles */

:root.dark-mode {
.themeToggle {
border-color: var(--grey-60);
background: var(--grey-90);
box-shadow: 0 1px 2px rgb(0 0 0 / 0.3);
}

.themeToggleButton {
color: var(--grey-40);
}

.themeToggleButton:hover:not(.themeToggleButton-active) {
background: var(--grey-80);
}

.themeToggleButton-active {
background: var(--grey-70);
box-shadow:
0 1px 3px rgb(0 0 0 / 0.4),
0 1px 2px rgb(0 0 0 / 0.3);
color: white;
}

.themeToggleButton-active:hover {
background: var(--grey-70);
}
}
119 changes: 119 additions & 0 deletions src/components/app/ThemeToggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import * as React from 'react';
import { PureComponent } from 'react';
import { Localized } from '@fluent/react';
import type { ThemePreference } from 'firefox-profiler/utils/dark-mode';
import {
getThemePreference,
setThemePreference,
} from 'firefox-profiler/utils/dark-mode';

import './ThemeToggle.css';

type State = {
currentTheme: ThemePreference;
};

class ThemeToggle extends PureComponent<{}, State> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A new component but no hooks? I mean that's fine, I'm just surprised.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heh yeah. well, I feel like it's better to continue using the existing patterns that we have, but we need to do the switch at some point. It could actually be nicer to handle the cleanup for these, but I'll defer that to a follow-up.

override state = {
currentTheme: getThemePreference(),
};

override componentDidMount() {
// Listen for storage events (cross-tab sync)
window.addEventListener('storage', this._handleStorageChange);

// Listen for system preference changes
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
mediaQuery.addEventListener('change', this._handleSystemPreferenceChange);
}

override componentWillUnmount() {
window.removeEventListener('storage', this._handleStorageChange);

const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
mediaQuery.removeEventListener(
'change',
this._handleSystemPreferenceChange
);
}

_handleStorageChange = (e: StorageEvent) => {
if (e.key === 'theme' || e.key === null) {
this.setState({ currentTheme: getThemePreference() });
}
};

_handleSystemPreferenceChange = () => {
// Update state if user is on system preference
if (this.state.currentTheme === 'system') {
this.setState({ currentTheme: getThemePreference() });
}
};

_handleLightClick = () => {
setThemePreference('light');
this.setState({ currentTheme: 'light' });
};

_handleSystemClick = () => {
setThemePreference('system');
this.setState({ currentTheme: 'system' });
};

_handleDarkClick = () => {
setThemePreference('dark');
this.setState({ currentTheme: 'dark' });
};

_renderIconButton(
theme: ThemePreference,
labelL10nId: string,
icon: React.ReactNode,
onClick: () => void
) {
const isActive = this.state.currentTheme === theme;
return (
<Localized id={labelL10nId} attrs={{ title: true }}>
<button
type="button"
className={`themeToggleButton ${isActive ? 'themeToggleButton-active' : ''}`}
onClick={onClick}
aria-label={theme}
>
{icon}
</button>
</Localized>
);
}

override render() {
return (
<div className="themeToggle">
{this._renderIconButton(
'light',
'ThemeToggle--light',
<span className="themeToggleIcon themeToggleIcon--light" />,
this._handleLightClick
)}
{this._renderIconButton(
'system',
'ThemeToggle--system',
<span className="themeToggleIcon themeToggleIcon--system" />,
this._handleSystemClick
)}
{this._renderIconButton(
'dark',
'ThemeToggle--dark',
<span className="themeToggleIcon themeToggleIcon--dark" />,
this._handleDarkClick
)}
</div>
);
}
}

export { ThemeToggle };
6 changes: 5 additions & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,11 @@ if (svgFiltersElement) {
const forcedColorsMql = window.matchMedia('(forced-colors: active)');
const darkSchemeMql = window.matchMedia('(prefers-color-scheme: dark)');
forcedColorsMql.addEventListener('change', defineSvgFiltersForColors);
darkSchemeMql.addEventListener('change', defineSvgFiltersForColors);
darkSchemeMql.addEventListener('change', () => {
defineSvgFiltersForColors();
// Re-apply theme when system preference changes (for 'system' mode users)
initTheme();
});
}

const store = createStore();
Expand Down
Loading