mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-02-18 13:29:36 +01: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
@ -17,7 +17,6 @@ import {
|
||||
FileId,
|
||||
NonDeletedExcalidrawElement,
|
||||
OrderedExcalidrawElement,
|
||||
Theme,
|
||||
} from "../packages/excalidraw/element/types";
|
||||
import { useCallbackRefState } from "../packages/excalidraw/hooks/useCallbackRefState";
|
||||
import { t } from "../packages/excalidraw/i18n";
|
||||
@ -124,6 +123,7 @@ import {
|
||||
exportToPlus,
|
||||
share,
|
||||
} from "../packages/excalidraw/components/icons";
|
||||
import { appThemeAtom, useHandleAppTheme } from "./useHandleAppTheme";
|
||||
|
||||
polyfill();
|
||||
|
||||
@ -303,6 +303,9 @@ const ExcalidrawWrapper = () => {
|
||||
const [langCode, setLangCode] = useAtom(appLangCodeAtom);
|
||||
const isCollabDisabled = isRunningInIframe();
|
||||
|
||||
const [appTheme, setAppTheme] = useAtom(appThemeAtom);
|
||||
const { editorTheme } = useHandleAppTheme();
|
||||
|
||||
// initial state
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@ -566,23 +569,6 @@ const ExcalidrawWrapper = () => {
|
||||
languageDetector.cacheUserLanguage(langCode);
|
||||
}, [langCode]);
|
||||
|
||||
const [theme, setTheme] = useState<Theme>(
|
||||
() =>
|
||||
(localStorage.getItem(
|
||||
STORAGE_KEYS.LOCAL_STORAGE_THEME,
|
||||
) as Theme | null) ||
|
||||
// FIXME migration from old LS scheme. Can be removed later. #5660
|
||||
importFromLocalStorage().appState?.theme ||
|
||||
THEME.LIGHT,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(STORAGE_KEYS.LOCAL_STORAGE_THEME, theme);
|
||||
// currently only used for body styling during init (see public/index.html),
|
||||
// but may change in the future
|
||||
document.documentElement.classList.toggle("dark", theme === THEME.DARK);
|
||||
}, [theme]);
|
||||
|
||||
const onChange = (
|
||||
elements: readonly OrderedExcalidrawElement[],
|
||||
appState: AppState,
|
||||
@ -592,8 +578,6 @@ const ExcalidrawWrapper = () => {
|
||||
collabAPI.syncElements(elements);
|
||||
}
|
||||
|
||||
setTheme(appState.theme);
|
||||
|
||||
// this check is redundant, but since this is a hot path, it's best
|
||||
// not to evaludate the nested expression every time
|
||||
if (!LocalData.isSavePaused()) {
|
||||
@ -798,7 +782,7 @@ const ExcalidrawWrapper = () => {
|
||||
detectScroll={false}
|
||||
handleKeyboardGlobally={true}
|
||||
autoFocus={true}
|
||||
theme={theme}
|
||||
theme={editorTheme}
|
||||
renderTopRightUI={(isMobile) => {
|
||||
if (isMobile || !collabAPI || isCollabDisabled) {
|
||||
return null;
|
||||
@ -820,6 +804,8 @@ const ExcalidrawWrapper = () => {
|
||||
onCollabDialogOpen={onCollabDialogOpen}
|
||||
isCollaborating={isCollaborating}
|
||||
isCollabEnabled={!isCollabDisabled}
|
||||
theme={appTheme}
|
||||
setTheme={(theme) => setAppTheme(theme)}
|
||||
/>
|
||||
<AppWelcomeScreen
|
||||
onCollabDialogOpen={onCollabDialogOpen}
|
||||
@ -1093,7 +1079,14 @@ const ExcalidrawWrapper = () => {
|
||||
}
|
||||
},
|
||||
},
|
||||
CommandPalette.defaultItems.toggleTheme,
|
||||
{
|
||||
...CommandPalette.defaultItems.toggleTheme,
|
||||
perform: () => {
|
||||
setAppTheme(
|
||||
editorTheme === THEME.DARK ? THEME.LIGHT : THEME.DARK,
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Excalidraw>
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import { PlusPromoIcon } from "../../packages/excalidraw/components/icons";
|
||||
import { Theme } from "../../packages/excalidraw/element/types";
|
||||
import { MainMenu } from "../../packages/excalidraw/index";
|
||||
import { LanguageList } from "./LanguageList";
|
||||
|
||||
@ -7,6 +8,8 @@ export const AppMainMenu: React.FC<{
|
||||
onCollabDialogOpen: () => any;
|
||||
isCollaborating: boolean;
|
||||
isCollabEnabled: boolean;
|
||||
theme: Theme | "system";
|
||||
setTheme: (theme: Theme | "system") => void;
|
||||
}> = React.memo((props) => {
|
||||
return (
|
||||
<MainMenu>
|
||||
@ -35,7 +38,11 @@ export const AppMainMenu: React.FC<{
|
||||
</MainMenu.ItemLink>
|
||||
<MainMenu.DefaultItems.Socials />
|
||||
<MainMenu.Separator />
|
||||
<MainMenu.DefaultItems.ToggleTheme />
|
||||
<MainMenu.DefaultItems.ToggleTheme
|
||||
allowSystemTheme
|
||||
theme={props.theme}
|
||||
onSelect={props.setTheme}
|
||||
/>
|
||||
<MainMenu.ItemCustom>
|
||||
<LanguageList style={{ width: "100%" }} />
|
||||
</MainMenu.ItemCustom>
|
||||
|
@ -64,12 +64,30 @@
|
||||
<!-- to minimize white flash on load when user has dark mode enabled -->
|
||||
<script>
|
||||
try {
|
||||
//
|
||||
const theme = window.localStorage.getItem("excalidraw-theme");
|
||||
if (theme === "dark") {
|
||||
document.documentElement.classList.add("dark");
|
||||
function setTheme(theme) {
|
||||
if (theme === "dark") {
|
||||
document.documentElement.classList.add("dark");
|
||||
} else {
|
||||
document.documentElement.classList.remove("dark");
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
function getTheme() {
|
||||
const theme = window.localStorage.getItem("excalidraw-theme");
|
||||
|
||||
if (theme && theme === "system") {
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light";
|
||||
} else {
|
||||
return theme || "light";
|
||||
}
|
||||
}
|
||||
|
||||
setTheme(getTheme());
|
||||
} catch (e) {
|
||||
console.error("Error setting dark mode", e);
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
html.dark {
|
||||
|
70
excalidraw-app/useHandleAppTheme.ts
Normal file
70
excalidraw-app/useHandleAppTheme.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import { atom, useAtom } from "jotai";
|
||||
import { useEffect, useLayoutEffect, useState } from "react";
|
||||
import { THEME } from "../packages/excalidraw";
|
||||
import { EVENT } from "../packages/excalidraw/constants";
|
||||
import { Theme } from "../packages/excalidraw/element/types";
|
||||
import { KEYS } from "../packages/excalidraw/keys";
|
||||
import { STORAGE_KEYS } from "./app_constants";
|
||||
|
||||
export const appThemeAtom = atom<Theme | "system">(
|
||||
(localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_THEME) as
|
||||
| Theme
|
||||
| "system"
|
||||
| null) || THEME.LIGHT,
|
||||
);
|
||||
|
||||
const getDarkThemeMediaQuery = (): MediaQueryList | undefined =>
|
||||
window.matchMedia?.("(prefers-color-scheme: dark)");
|
||||
|
||||
export const useHandleAppTheme = () => {
|
||||
const [appTheme, setAppTheme] = useAtom(appThemeAtom);
|
||||
const [editorTheme, setEditorTheme] = useState<Theme>(THEME.LIGHT);
|
||||
|
||||
useEffect(() => {
|
||||
const mediaQuery = getDarkThemeMediaQuery();
|
||||
|
||||
const handleChange = (e: MediaQueryListEvent) => {
|
||||
setEditorTheme(e.matches ? THEME.DARK : THEME.LIGHT);
|
||||
};
|
||||
|
||||
if (appTheme === "system") {
|
||||
mediaQuery?.addEventListener("change", handleChange);
|
||||
}
|
||||
|
||||
const handleKeydown = (event: KeyboardEvent) => {
|
||||
if (
|
||||
!event[KEYS.CTRL_OR_CMD] &&
|
||||
event.altKey &&
|
||||
event.shiftKey &&
|
||||
event.code === KEYS.D
|
||||
) {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
setAppTheme(editorTheme === THEME.DARK ? THEME.LIGHT : THEME.DARK);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener(EVENT.KEYDOWN, handleKeydown, { capture: true });
|
||||
|
||||
return () => {
|
||||
mediaQuery?.removeEventListener("change", handleChange);
|
||||
document.removeEventListener(EVENT.KEYDOWN, handleKeydown, {
|
||||
capture: true,
|
||||
});
|
||||
};
|
||||
}, [appTheme, editorTheme, setAppTheme]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
localStorage.setItem(STORAGE_KEYS.LOCAL_STORAGE_THEME, appTheme);
|
||||
|
||||
if (appTheme === "system") {
|
||||
setEditorTheme(
|
||||
getDarkThemeMediaQuery()?.matches ? THEME.DARK : THEME.LIGHT,
|
||||
);
|
||||
} else {
|
||||
setEditorTheme(appTheme);
|
||||
}
|
||||
}, [appTheme]);
|
||||
|
||||
return { editorTheme };
|
||||
};
|
@ -15,6 +15,7 @@ Please add the latest change on the top under the correct section.
|
||||
|
||||
### Features
|
||||
|
||||
- `MainMenu.DefaultItems.ToggleTheme` now supports `onSelect(theme: string)` callback, and optionally `allowSystemTheme: boolean` alongside `theme: string` to indicate you want to allow users to set to system theme (you need to handle this yourself). [#7853](https://github.com/excalidraw/excalidraw/pull/7853)
|
||||
- Add `useHandleLibrary`'s `opts.adapter` as the new recommended pattern to handle library initialization and persistence on library updates. [#7655](https://github.com/excalidraw/excalidraw/pull/7655)
|
||||
- Add `useHandleLibrary`'s `opts.migrationAdapter` adapter to handle library migration during init, when migrating from one data store to another (e.g. from LocalStorage to IndexedDB). [#7655](https://github.com/excalidraw/excalidraw/pull/7655)
|
||||
- Soft-deprecate `useHandleLibrary`'s `opts.getInitialLibraryItems` in favor of `opts.adapter`. [#7655](https://github.com/excalidraw/excalidraw/pull/7655)
|
||||
|
@ -432,7 +432,9 @@ export const actionZoomToFit = register({
|
||||
export const actionToggleTheme = register({
|
||||
name: "toggleTheme",
|
||||
label: (_, appState) => {
|
||||
return appState.theme === "dark" ? "buttons.lightMode" : "buttons.darkMode";
|
||||
return appState.theme === THEME.DARK
|
||||
? "buttons.lightMode"
|
||||
: "buttons.darkMode";
|
||||
},
|
||||
keywords: ["toggle", "dark", "light", "mode", "theme"],
|
||||
icon: (appState) => (appState.theme === THEME.LIGHT ? MoonIcon : SunIcon),
|
||||
|
@ -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>
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { THEME } from "../constants";
|
||||
import { Theme } from "../element/types";
|
||||
import { DataURL } from "../types";
|
||||
import { OpenAIInput, OpenAIOutput } from "./ai/types";
|
||||
@ -39,7 +40,7 @@ export async function diagramToHTML({
|
||||
image,
|
||||
apiKey,
|
||||
text,
|
||||
theme = "light",
|
||||
theme = THEME.LIGHT,
|
||||
}: {
|
||||
image: DataURL;
|
||||
apiKey: string;
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { useState, useLayoutEffect } from "react";
|
||||
import { useDevice, useExcalidrawContainer } from "../components/App";
|
||||
import { THEME } from "../constants";
|
||||
import { useUIAppState } from "../context/ui-appState";
|
||||
|
||||
export const useCreatePortalContainer = (opts?: {
|
||||
@ -18,7 +19,7 @@ export const useCreatePortalContainer = (opts?: {
|
||||
div.className = "";
|
||||
div.classList.add("excalidraw", ...(opts?.className?.split(/\s+/) || []));
|
||||
div.classList.toggle("excalidraw--mobile", device.editor.isMobile);
|
||||
div.classList.toggle("theme--dark", theme === "dark");
|
||||
div.classList.toggle("theme--dark", theme === THEME.DARK);
|
||||
}
|
||||
}, [div, theme, device.editor.isMobile, opts?.className]);
|
||||
|
||||
|
@ -110,6 +110,7 @@
|
||||
"showStroke": "Show stroke color picker",
|
||||
"showBackground": "Show background color picker",
|
||||
"toggleTheme": "Toggle light/dark theme",
|
||||
"theme": "Theme",
|
||||
"personalLib": "Personal Library",
|
||||
"excalidrawLib": "Excalidraw Library",
|
||||
"decreaseFontSize": "Decrease font size",
|
||||
@ -180,6 +181,7 @@
|
||||
"fullScreen": "Full screen",
|
||||
"darkMode": "Dark mode",
|
||||
"lightMode": "Light mode",
|
||||
"systemMode": "System mode",
|
||||
"zenMode": "Zen mode",
|
||||
"objectsSnapMode": "Snap to objects",
|
||||
"exitZenMode": "Exit zen mode",
|
||||
|
@ -2,7 +2,7 @@ import { StaticCanvasAppState, AppState } from "../types";
|
||||
|
||||
import { StaticCanvasRenderConfig } from "../scene/types";
|
||||
|
||||
import { THEME_FILTER } from "../constants";
|
||||
import { THEME, THEME_FILTER } from "../constants";
|
||||
|
||||
export const fillCircle = (
|
||||
context: CanvasRenderingContext2D,
|
||||
@ -49,7 +49,7 @@ export const bootstrapCanvas = ({
|
||||
context.setTransform(1, 0, 0, 1, 0, 0);
|
||||
context.scale(scale, scale);
|
||||
|
||||
if (isExporting && theme === "dark") {
|
||||
if (isExporting && theme === THEME.DARK) {
|
||||
context.filter = THEME_FILTER;
|
||||
}
|
||||
|
||||
|
@ -41,6 +41,7 @@ import {
|
||||
ELEMENT_READY_TO_ERASE_OPACITY,
|
||||
FRAME_STYLE,
|
||||
MIME_TYPES,
|
||||
THEME,
|
||||
} from "../constants";
|
||||
import { getStroke, StrokeOptions } from "perfect-freehand";
|
||||
import {
|
||||
@ -79,7 +80,7 @@ const shouldResetImageFilter = (
|
||||
appState: StaticCanvasAppState,
|
||||
) => {
|
||||
return (
|
||||
appState.theme === "dark" &&
|
||||
appState.theme === THEME.DARK &&
|
||||
isInitializedImageElement(element) &&
|
||||
!isPendingImageElement(element, renderConfig) &&
|
||||
renderConfig.imageCache.get(element.fileId)?.mimeType !== MIME_TYPES.svg
|
||||
@ -668,7 +669,7 @@ export const renderElement = (
|
||||
// TODO change later to only affect AI frames
|
||||
if (isMagicFrameElement(element)) {
|
||||
context.strokeStyle =
|
||||
appState.theme === "light" ? "#7affd7" : "#1d8264";
|
||||
appState.theme === THEME.LIGHT ? "#7affd7" : "#1d8264";
|
||||
}
|
||||
|
||||
if (FRAME_STYLE.radius && context.roundRect) {
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { THEME } from "../constants";
|
||||
import { PointSnapLine, PointerSnapLine } from "../snapping";
|
||||
import { InteractiveCanvasAppState, Point } from "../types";
|
||||
|
||||
@ -18,7 +19,7 @@ export const renderSnaps = (
|
||||
// Don't change if zen mode, because we draw only crosses, we want the
|
||||
// colors to be more visible
|
||||
const snapColor =
|
||||
appState.theme === "light" || appState.zenModeEnabled
|
||||
appState.theme === THEME.LIGHT || appState.zenModeEnabled
|
||||
? SNAP_COLOR_LIGHT
|
||||
: SNAP_COLOR_DARK;
|
||||
// in zen mode make the cross more visible since we don't draw the lines
|
||||
|
@ -19,6 +19,7 @@ import {
|
||||
FONT_FAMILY,
|
||||
FRAME_STYLE,
|
||||
SVG_NS,
|
||||
THEME,
|
||||
THEME_FILTER,
|
||||
} from "../constants";
|
||||
import { getDefaultAppState } from "../appState";
|
||||
@ -237,7 +238,7 @@ export const exportToCanvas = async (
|
||||
scrollY: -minY + exportPadding,
|
||||
zoom: defaultAppState.zoom,
|
||||
shouldCacheIgnoreZoom: false,
|
||||
theme: appState.exportWithDarkMode ? "dark" : "light",
|
||||
theme: appState.exportWithDarkMode ? THEME.DARK : THEME.LIGHT,
|
||||
},
|
||||
renderConfig: {
|
||||
canvasBackgroundColor: viewBackgroundColor,
|
||||
|
@ -11,6 +11,20 @@ require("fake-indexeddb/auto");
|
||||
|
||||
polyfill();
|
||||
|
||||
Object.defineProperty(window, "matchMedia", {
|
||||
writable: true,
|
||||
value: vi.fn().mockImplementation((query) => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: vi.fn(), // deprecated
|
||||
removeListener: vi.fn(), // deprecated
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
})),
|
||||
});
|
||||
|
||||
vi.mock("nanoid", () => {
|
||||
return {
|
||||
nanoid: vi.fn(() => "test-id"),
|
||||
|
Loading…
Reference in New Issue
Block a user