mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
feat: add system mode to the theme selector (#7853)
Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
This commit is contained in:
parent
92bc08207c
commit
cd50aa719f
21 changed files with 301 additions and 56 deletions
|
@ -1014,7 +1014,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
width: 100%;
|
||||
height: 100%;
|
||||
color: ${
|
||||
this.state.theme === "dark" ? "white" : "black"
|
||||
this.state.theme === THEME.DARK ? "white" : "black"
|
||||
};
|
||||
}
|
||||
body {
|
||||
|
@ -1281,7 +1281,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
return null;
|
||||
}
|
||||
|
||||
const isDarkTheme = this.state.theme === "dark";
|
||||
const isDarkTheme = this.state.theme === THEME.DARK;
|
||||
|
||||
let frameIndex = 0;
|
||||
let magicFrameIndex = 0;
|
||||
|
@ -2730,7 +2730,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
|
||||
this.excalidrawContainerRef.current?.classList.toggle(
|
||||
"theme--dark",
|
||||
this.state.theme === "dark",
|
||||
this.state.theme === THEME.DARK,
|
||||
);
|
||||
|
||||
if (
|
||||
|
|
|
@ -14,7 +14,9 @@ export const DarkModeToggle = (props: {
|
|||
}) => {
|
||||
const title =
|
||||
props.title ||
|
||||
(props.value === "dark" ? t("buttons.lightMode") : t("buttons.darkMode"));
|
||||
(props.value === THEME.DARK
|
||||
? t("buttons.lightMode")
|
||||
: t("buttons.darkMode"));
|
||||
|
||||
return (
|
||||
<ToolButton
|
||||
|
|
|
@ -3,7 +3,8 @@ import "./RadioGroup.scss";
|
|||
|
||||
export type RadioGroupChoice<T> = {
|
||||
value: T;
|
||||
label: string;
|
||||
label: React.ReactNode;
|
||||
ariaLabel?: string;
|
||||
};
|
||||
|
||||
export type RadioGroupProps<T> = {
|
||||
|
@ -26,13 +27,15 @@ export const RadioGroup = function <T>({
|
|||
className={clsx("RadioGroup__choice", {
|
||||
active: choice.value === value,
|
||||
})}
|
||||
key={choice.label}
|
||||
key={String(choice.value)}
|
||||
title={choice.ariaLabel}
|
||||
>
|
||||
<input
|
||||
name={name}
|
||||
type="radio"
|
||||
checked={choice.value === value}
|
||||
onChange={() => onChange(choice.value)}
|
||||
aria-label={choice.ariaLabel}
|
||||
/>
|
||||
{choice.label}
|
||||
</div>
|
||||
|
|
|
@ -75,6 +75,12 @@
|
|||
&__shortcut {
|
||||
margin-inline-start: auto;
|
||||
opacity: 0.5;
|
||||
|
||||
&--orphaned {
|
||||
text-align: right;
|
||||
font-size: 0.875rem;
|
||||
padding: 0 0.625rem;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
@ -94,6 +100,22 @@
|
|||
}
|
||||
}
|
||||
|
||||
.dropdown-menu-item-bare {
|
||||
align-items: center;
|
||||
height: 2rem;
|
||||
justify-content: space-between;
|
||||
|
||||
@media screen and (min-width: 1921px) {
|
||||
height: 2.25rem;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu-item-custom {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
import { useDevice } from "../App";
|
||||
import { RadioGroup } from "../RadioGroup";
|
||||
|
||||
type Props<T> = {
|
||||
value: T;
|
||||
shortcut?: string;
|
||||
choices: {
|
||||
value: T;
|
||||
label: React.ReactNode;
|
||||
ariaLabel?: string;
|
||||
}[];
|
||||
onChange: (value: T) => void;
|
||||
children: React.ReactNode;
|
||||
name: string;
|
||||
};
|
||||
|
||||
const DropdownMenuItemContentRadio = <T,>({
|
||||
value,
|
||||
shortcut,
|
||||
onChange,
|
||||
choices,
|
||||
children,
|
||||
name,
|
||||
}: Props<T>) => {
|
||||
const device = useDevice();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="dropdown-menu-item-base dropdown-menu-item-bare">
|
||||
<label className="dropdown-menu-item__text" htmlFor={name}>
|
||||
{children}
|
||||
</label>
|
||||
<RadioGroup
|
||||
name={name}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
choices={choices}
|
||||
/>
|
||||
</div>
|
||||
{shortcut && !device.editor.isMobile && (
|
||||
<div className="dropdown-menu-item__shortcut dropdown-menu-item__shortcut--orphaned">
|
||||
{shortcut}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
DropdownMenuItemContentRadio.displayName = "DropdownMenuItemContentRadio";
|
||||
|
||||
export default DropdownMenuItemContentRadio;
|
|
@ -433,15 +433,10 @@ export const MoonIcon = createIcon(
|
|||
);
|
||||
|
||||
export const SunIcon = createIcon(
|
||||
<g
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.25"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<g stroke="currentColor" strokeLinejoin="round">
|
||||
<path d="M10 12.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5ZM10 4.167V2.5M14.167 5.833l1.166-1.166M15.833 10H17.5M14.167 14.167l1.166 1.166M10 15.833V17.5M5.833 14.167l-1.166 1.166M5 10H3.333M5.833 5.833 4.667 4.667" />
|
||||
</g>,
|
||||
modifiedTablerIconProps,
|
||||
{ ...modifiedTablerIconProps, strokeWidth: 1.5 },
|
||||
);
|
||||
|
||||
export const HamburgerMenuIcon = createIcon(
|
||||
|
@ -2092,3 +2087,11 @@ export const coffeeIcon = createIcon(
|
|||
</g>,
|
||||
tablerIconProps,
|
||||
);
|
||||
|
||||
export const DeviceDesktopIcon = createIcon(
|
||||
<g stroke="currentColor">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M3 5a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1h-16a1 1 0 0 1-1-1v-10zM7 20h10M9 16v4M15 16v4" />
|
||||
</g>,
|
||||
{ ...tablerIconProps, strokeWidth: 1.5 },
|
||||
);
|
||||
|
|
|
@ -8,6 +8,7 @@ import {
|
|||
} from "../App";
|
||||
import {
|
||||
boltIcon,
|
||||
DeviceDesktopIcon,
|
||||
ExportIcon,
|
||||
ExportImageIcon,
|
||||
HelpIcon,
|
||||
|
@ -35,6 +36,9 @@ import { jotaiScope } from "../../jotai";
|
|||
import { useUIAppState } from "../../context/ui-appState";
|
||||
import { openConfirmModal } from "../OverwriteConfirm/OverwriteConfirmState";
|
||||
import Trans from "../Trans";
|
||||
import DropdownMenuItemContentRadio from "../dropdownMenu/DropdownMenuItemContentRadio";
|
||||
import { THEME } from "../../constants";
|
||||
import type { Theme } from "../../element/types";
|
||||
|
||||
import "./DefaultItems.scss";
|
||||
|
||||
|
@ -181,32 +185,80 @@ export const ClearCanvas = () => {
|
|||
};
|
||||
ClearCanvas.displayName = "ClearCanvas";
|
||||
|
||||
export const ToggleTheme = () => {
|
||||
export const ToggleTheme = (
|
||||
props:
|
||||
| {
|
||||
allowSystemTheme: true;
|
||||
theme: Theme | "system";
|
||||
onSelect: (theme: Theme | "system") => void;
|
||||
}
|
||||
| {
|
||||
allowSystemTheme?: false;
|
||||
onSelect?: (theme: Theme) => void;
|
||||
},
|
||||
) => {
|
||||
const { t } = useI18n();
|
||||
const appState = useUIAppState();
|
||||
const actionManager = useExcalidrawActionManager();
|
||||
const shortcut = getShortcutFromShortcutName("toggleTheme");
|
||||
|
||||
if (!actionManager.isActionEnabled(actionToggleTheme)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (props?.allowSystemTheme) {
|
||||
return (
|
||||
<DropdownMenuItemContentRadio
|
||||
name="theme"
|
||||
value={props.theme}
|
||||
onChange={(value: Theme | "system") => props.onSelect(value)}
|
||||
choices={[
|
||||
{
|
||||
value: THEME.LIGHT,
|
||||
label: SunIcon,
|
||||
ariaLabel: `${t("buttons.lightMode")} - ${shortcut}`,
|
||||
},
|
||||
{
|
||||
value: THEME.DARK,
|
||||
label: MoonIcon,
|
||||
ariaLabel: `${t("buttons.darkMode")} - ${shortcut}`,
|
||||
},
|
||||
{
|
||||
value: "system",
|
||||
label: DeviceDesktopIcon,
|
||||
ariaLabel: t("buttons.systemMode"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
{t("labels.theme")}
|
||||
</DropdownMenuItemContentRadio>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
onSelect={(event) => {
|
||||
// do not close the menu when changing theme
|
||||
event.preventDefault();
|
||||
return actionManager.executeAction(actionToggleTheme);
|
||||
|
||||
if (props?.onSelect) {
|
||||
props.onSelect(
|
||||
appState.theme === THEME.DARK ? THEME.LIGHT : THEME.DARK,
|
||||
);
|
||||
} else {
|
||||
return actionManager.executeAction(actionToggleTheme);
|
||||
}
|
||||
}}
|
||||
icon={appState.theme === "dark" ? SunIcon : MoonIcon}
|
||||
icon={appState.theme === THEME.DARK ? SunIcon : MoonIcon}
|
||||
data-testid="toggle-dark-mode"
|
||||
shortcut={getShortcutFromShortcutName("toggleTheme")}
|
||||
shortcut={shortcut}
|
||||
aria-label={
|
||||
appState.theme === "dark"
|
||||
appState.theme === THEME.DARK
|
||||
? t("buttons.lightMode")
|
||||
: t("buttons.darkMode")
|
||||
}
|
||||
>
|
||||
{appState.theme === "dark"
|
||||
{appState.theme === THEME.DARK
|
||||
? t("buttons.lightMode")
|
||||
: t("buttons.darkMode")}
|
||||
</DropdownMenuItem>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue