Skip to content

feat: primer color schemes #1752

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jan 23, 2025
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: 8 additions & 2 deletions src/main/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,15 @@ app.whenReady().then(async () => {

nativeTheme.on('updated', () => {
if (nativeTheme.shouldUseDarkColors) {
mb.window.webContents.send(namespacedEvent('update-theme'), 'DARK');
mb.window.webContents.send(
namespacedEvent('update-theme'),
'DARK_DEFAULT',
);
} else {
mb.window.webContents.send(namespacedEvent('update-theme'), 'LIGHT');
mb.window.webContents.send(
namespacedEvent('update-theme'),
'LIGHT_DEFAULT',
);
}
});

Expand Down
8 changes: 7 additions & 1 deletion src/renderer/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,15 @@
@import "@primer/primitives/dist/css/functional/typography/typography.css";

/* Themes and Colors */
/* TODO Add support for setting color modes (dark_dimmed) - see #1748 */
@import "@primer/primitives/dist/css/functional/themes/light.css";
@import "@primer/primitives/dist/css/functional/themes/light-tritanopia.css";
@import "@primer/primitives/dist/css/functional/themes/light-high-contrast.css";
@import "@primer/primitives/dist/css/functional/themes/light-colorblind.css";
@import "@primer/primitives/dist/css/functional/themes/dark.css";
@import "@primer/primitives/dist/css/functional/themes/dark-colorblind.css";
@import "@primer/primitives/dist/css/functional/themes/dark-dimmed.css";
@import "@primer/primitives/dist/css/functional/themes/dark-high-contrast.css";
@import "@primer/primitives/dist/css/functional/themes/dark-tritanopia.css";

html,
body,
Expand Down
11 changes: 9 additions & 2 deletions src/renderer/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ import { NotificationsRoute } from './routes/Notifications';
import { SettingsRoute } from './routes/Settings';

import './App.css';
import {
DEFAULT_DAY_COLOR_SCHEME,
DEFAULT_NIGHT_COLOR_SCHEME,
} from './utils/theme';

function RequireAuth({ children }) {
const { isLoggedIn } = useContext(AppContext);
Expand All @@ -35,8 +39,11 @@ function RequireAuth({ children }) {

export const App = () => {
return (
// TODO Add support for setting color modes (dark_dimmed) - see #1748
<ThemeProvider dayScheme="light" nightScheme="dark">
<ThemeProvider
colorMode="auto"
dayScheme={DEFAULT_DAY_COLOR_SCHEME}
nightScheme={DEFAULT_NIGHT_COLOR_SCHEME}
>
<BaseStyles>
<AppProvider>
<Router>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ describe('renderer/routes/components/settings/AppearanceSettings.tsx', () => {
jest.clearAllMocks();
});

it('should change the theme radio group', async () => {
it('should change the theme mode dropdown', async () => {
await act(async () => {
render(
<AppContext.Provider
Expand All @@ -34,7 +34,8 @@ describe('renderer/routes/components/settings/AppearanceSettings.tsx', () => {
);
});

fireEvent.click(screen.getByLabelText('Light'));
const select = screen.getByTestId('settings-theme') as HTMLSelectElement;
fireEvent.change(select, { target: { value: 'LIGHT' } });

expect(updateSetting).toHaveBeenCalledTimes(1);
expect(updateSetting).toHaveBeenCalledWith('theme', 'LIGHT');
Expand Down
81 changes: 60 additions & 21 deletions src/renderer/components/settings/AppearanceSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,34 @@ import {
TagIcon,
XCircleIcon,
} from '@primer/octicons-react';
import { Button, ButtonGroup, IconButton, useTheme } from '@primer/react';
import {
Button,
ButtonGroup,
IconButton,
Select,
useTheme,
} from '@primer/react';

import { namespacedEvent } from '../../../shared/events';
import { AppContext } from '../../context/App';
import { Size, Theme } from '../../types';
import { hasMultipleAccounts } from '../../utils/auth/utils';
import { getColorModeFromTheme, setTheme } from '../../utils/theme';
import {
DEFAULT_DAY_COLOR_SCHEME,
DEFAULT_NIGHT_COLOR_SCHEME,
isDayScheme,
setScrollbarTheme,
} from '../../utils/theme';
import { zoomLevelToPercentage, zoomPercentageToLevel } from '../../utils/zoom';
import { Checkbox } from '../fields/Checkbox';
import { FieldLabel } from '../fields/FieldLabel';
import { RadioGroup } from '../fields/RadioGroup';
import { Title } from '../primitives/Title';

let timeout: NodeJS.Timeout;
const DELAY = 200;

export const AppearanceSettings: FC = () => {
const { setColorMode } = useTheme();
const { setColorMode, setDayScheme, setNightScheme } = useTheme();
const { auth, settings, updateSetting } = useContext(AppContext);
const [zoomPercentage, setZoomPercentage] = useState(
zoomLevelToPercentage(webFrame.getZoomLevel()),
Expand All @@ -41,14 +51,15 @@ export const AppearanceSettings: FC = () => {
namespacedEvent('update-theme'),
(_, updatedTheme: Theme) => {
if (settings.theme === Theme.SYSTEM) {
const mode = getColorModeFromTheme(updatedTheme);

setTheme(updatedTheme);
setColorMode(mode);
const mode = isDayScheme(updatedTheme) ? 'day' : 'night';
setColorMode('auto');
setDayScheme(DEFAULT_DAY_COLOR_SCHEME);
setNightScheme(DEFAULT_NIGHT_COLOR_SCHEME);
setScrollbarTheme(mode);
}
},
);
}, [settings.theme, setColorMode]);
}, [settings.theme, setColorMode, setDayScheme, setNightScheme]);

window.addEventListener('resize', () => {
// clear the timeout
Expand All @@ -65,20 +76,48 @@ export const AppearanceSettings: FC = () => {
<fieldset>
<Title icon={PaintbrushIcon}>Appearance</Title>

<RadioGroup
name="theme"
label="Theme:"
value={settings.theme}
options={[
{ label: 'System', value: Theme.SYSTEM },
{ label: 'Light', value: Theme.LIGHT },
{ label: 'Dark', value: Theme.DARK },
]}
onChange={(evt) => updateSetting('theme', evt.target.value as Theme)}
/>
<div className="flex items-center mt-3 mb-2 text-sm">
<FieldLabel name="theme" label="Theme:" />

<Select
id="theme"
value={settings.theme}
onChange={(evt) => updateSetting('theme', evt.target.value as Theme)}
data-testid="settings-theme"
>
<Select.OptGroup label="System">
<Select.Option value={Theme.SYSTEM}>System</Select.Option>
</Select.OptGroup>
<Select.OptGroup label="Light">
<Select.Option value={Theme.LIGHT}>Light default</Select.Option>
<Select.Option value={Theme.LIGHT_HIGH_CONTRAST}>
Light high contrast
</Select.Option>
<Select.Option value={Theme.LIGHT_COLORBLIND}>
Light Protanopia & Deuteranopia
</Select.Option>
<Select.Option value={Theme.LIGHT_TRITANOPIA}>
Light Tritanopia
</Select.Option>
</Select.OptGroup>
<Select.OptGroup label="Dark">
<Select.Option value={Theme.DARK}>Dark default</Select.Option>
<Select.Option value={Theme.DARK_HIGH_CONTRAST}>
Dark high contrast
</Select.Option>
<Select.Option value={Theme.DARK_COLORBLIND}>
Dark Protanopia & Deuteranopia
</Select.Option>
<Select.Option value={Theme.DARK_TRITANOPIA}>
Dark Tritanopia
</Select.Option>
<Select.Option value={Theme.DARK_DIMMED}>Dark dimmed</Select.Option>
</Select.OptGroup>
</Select>
</div>

<div className="flex items-center mt-3 mb-2 text-sm">
<FieldLabel name="zoom" label="Zoom" />
<FieldLabel name="zoom" label="Zoom:" />

<ButtonGroup>
<IconButton
Expand Down
25 changes: 19 additions & 6 deletions src/renderer/context/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,13 @@ import {
import { Constants } from '../utils/constants';
import { getNotificationCount } from '../utils/notifications/notifications';
import { clearState, loadState, saveState } from '../utils/storage';
import { getColorModeFromTheme, setTheme } from '../utils/theme';
import {
DEFAULT_DAY_COLOR_SCHEME,
DEFAULT_NIGHT_COLOR_SCHEME,
isDayScheme,
mapThemeModeToColorScheme,
setScrollbarTheme,
} from '../utils/theme';
import { zoomPercentageToLevel } from '../utils/zoom';

export const defaultAuth: AuthState = {
Expand Down Expand Up @@ -125,7 +131,7 @@ interface AppContextState {
export const AppContext = createContext<Partial<AppContextState>>({});

export const AppProvider = ({ children }: { children: ReactNode }) => {
const { setColorMode } = useTheme();
const { setColorMode, setDayScheme, setNightScheme } = useTheme();
const [auth, setAuth] = useState<AuthState>(defaultAuth);
const [settings, setSettings] = useState<SettingsState>(defaultSettings);
const {
Expand All @@ -144,11 +150,18 @@ export const AppProvider = ({ children }: { children: ReactNode }) => {
}, []);

useEffect(() => {
const mode = getColorModeFromTheme(settings.theme);
setColorMode(mode);
const colorScheme = mapThemeModeToColorScheme(settings.theme);

setTheme(settings.theme); // TODO - Replace fully with Primer design tokens and components
}, [settings.theme, setColorMode]);
if (isDayScheme(settings.theme)) {
setDayScheme(colorScheme ?? DEFAULT_DAY_COLOR_SCHEME);
setColorMode('day');
setScrollbarTheme('day');
} else {
setNightScheme(colorScheme ?? DEFAULT_NIGHT_COLOR_SCHEME);
setColorMode('night');
setScrollbarTheme('night');
}
}, [settings.theme, setColorMode, setDayScheme, setNightScheme]);

// biome-ignore lint/correctness/useExhaustiveDependencies: We only want fetchNotifications to be called for account changes
useEffect(() => {
Expand Down
Loading
Loading