1
0
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:
Arnost Pleskot 2024-04-08 16:46:24 +02:00 committed by GitHub
parent 92bc08207c
commit cd50aa719f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 301 additions and 56 deletions

@ -17,7 +17,6 @@ import {
FileId, FileId,
NonDeletedExcalidrawElement, NonDeletedExcalidrawElement,
OrderedExcalidrawElement, OrderedExcalidrawElement,
Theme,
} from "../packages/excalidraw/element/types"; } from "../packages/excalidraw/element/types";
import { useCallbackRefState } from "../packages/excalidraw/hooks/useCallbackRefState"; import { useCallbackRefState } from "../packages/excalidraw/hooks/useCallbackRefState";
import { t } from "../packages/excalidraw/i18n"; import { t } from "../packages/excalidraw/i18n";
@ -124,6 +123,7 @@ import {
exportToPlus, exportToPlus,
share, share,
} from "../packages/excalidraw/components/icons"; } from "../packages/excalidraw/components/icons";
import { appThemeAtom, useHandleAppTheme } from "./useHandleAppTheme";
polyfill(); polyfill();
@ -303,6 +303,9 @@ const ExcalidrawWrapper = () => {
const [langCode, setLangCode] = useAtom(appLangCodeAtom); const [langCode, setLangCode] = useAtom(appLangCodeAtom);
const isCollabDisabled = isRunningInIframe(); const isCollabDisabled = isRunningInIframe();
const [appTheme, setAppTheme] = useAtom(appThemeAtom);
const { editorTheme } = useHandleAppTheme();
// initial state // initial state
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -566,23 +569,6 @@ const ExcalidrawWrapper = () => {
languageDetector.cacheUserLanguage(langCode); languageDetector.cacheUserLanguage(langCode);
}, [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 = ( const onChange = (
elements: readonly OrderedExcalidrawElement[], elements: readonly OrderedExcalidrawElement[],
appState: AppState, appState: AppState,
@ -592,8 +578,6 @@ const ExcalidrawWrapper = () => {
collabAPI.syncElements(elements); collabAPI.syncElements(elements);
} }
setTheme(appState.theme);
// this check is redundant, but since this is a hot path, it's best // this check is redundant, but since this is a hot path, it's best
// not to evaludate the nested expression every time // not to evaludate the nested expression every time
if (!LocalData.isSavePaused()) { if (!LocalData.isSavePaused()) {
@ -798,7 +782,7 @@ const ExcalidrawWrapper = () => {
detectScroll={false} detectScroll={false}
handleKeyboardGlobally={true} handleKeyboardGlobally={true}
autoFocus={true} autoFocus={true}
theme={theme} theme={editorTheme}
renderTopRightUI={(isMobile) => { renderTopRightUI={(isMobile) => {
if (isMobile || !collabAPI || isCollabDisabled) { if (isMobile || !collabAPI || isCollabDisabled) {
return null; return null;
@ -820,6 +804,8 @@ const ExcalidrawWrapper = () => {
onCollabDialogOpen={onCollabDialogOpen} onCollabDialogOpen={onCollabDialogOpen}
isCollaborating={isCollaborating} isCollaborating={isCollaborating}
isCollabEnabled={!isCollabDisabled} isCollabEnabled={!isCollabDisabled}
theme={appTheme}
setTheme={(theme) => setAppTheme(theme)}
/> />
<AppWelcomeScreen <AppWelcomeScreen
onCollabDialogOpen={onCollabDialogOpen} onCollabDialogOpen={onCollabDialogOpen}
@ -1093,7 +1079,14 @@ const ExcalidrawWrapper = () => {
} }
}, },
}, },
CommandPalette.defaultItems.toggleTheme, {
...CommandPalette.defaultItems.toggleTheme,
perform: () => {
setAppTheme(
editorTheme === THEME.DARK ? THEME.LIGHT : THEME.DARK,
);
},
},
]} ]}
/> />
</Excalidraw> </Excalidraw>

@ -1,5 +1,6 @@
import React from "react"; import React from "react";
import { PlusPromoIcon } from "../../packages/excalidraw/components/icons"; import { PlusPromoIcon } from "../../packages/excalidraw/components/icons";
import { Theme } from "../../packages/excalidraw/element/types";
import { MainMenu } from "../../packages/excalidraw/index"; import { MainMenu } from "../../packages/excalidraw/index";
import { LanguageList } from "./LanguageList"; import { LanguageList } from "./LanguageList";
@ -7,6 +8,8 @@ export const AppMainMenu: React.FC<{
onCollabDialogOpen: () => any; onCollabDialogOpen: () => any;
isCollaborating: boolean; isCollaborating: boolean;
isCollabEnabled: boolean; isCollabEnabled: boolean;
theme: Theme | "system";
setTheme: (theme: Theme | "system") => void;
}> = React.memo((props) => { }> = React.memo((props) => {
return ( return (
<MainMenu> <MainMenu>
@ -35,7 +38,11 @@ export const AppMainMenu: React.FC<{
</MainMenu.ItemLink> </MainMenu.ItemLink>
<MainMenu.DefaultItems.Socials /> <MainMenu.DefaultItems.Socials />
<MainMenu.Separator /> <MainMenu.Separator />
<MainMenu.DefaultItems.ToggleTheme /> <MainMenu.DefaultItems.ToggleTheme
allowSystemTheme
theme={props.theme}
onSelect={props.setTheme}
/>
<MainMenu.ItemCustom> <MainMenu.ItemCustom>
<LanguageList style={{ width: "100%" }} /> <LanguageList style={{ width: "100%" }} />
</MainMenu.ItemCustom> </MainMenu.ItemCustom>

@ -64,12 +64,30 @@
<!-- to minimize white flash on load when user has dark mode enabled --> <!-- to minimize white flash on load when user has dark mode enabled -->
<script> <script>
try { try {
// function setTheme(theme) {
const theme = window.localStorage.getItem("excalidraw-theme");
if (theme === "dark") { if (theme === "dark") {
document.documentElement.classList.add("dark"); document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
}
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);
} }
} catch {}
</script> </script>
<style> <style>
html.dark { html.dark {

@ -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 ### 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.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) - 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) - 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({ export const actionToggleTheme = register({
name: "toggleTheme", name: "toggleTheme",
label: (_, appState) => { 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"], keywords: ["toggle", "dark", "light", "mode", "theme"],
icon: (appState) => (appState.theme === THEME.LIGHT ? MoonIcon : SunIcon), icon: (appState) => (appState.theme === THEME.LIGHT ? MoonIcon : SunIcon),

@ -1014,7 +1014,7 @@ class App extends React.Component<AppProps, AppState> {
width: 100%; width: 100%;
height: 100%; height: 100%;
color: ${ color: ${
this.state.theme === "dark" ? "white" : "black" this.state.theme === THEME.DARK ? "white" : "black"
}; };
} }
body { body {
@ -1281,7 +1281,7 @@ class App extends React.Component<AppProps, AppState> {
return null; return null;
} }
const isDarkTheme = this.state.theme === "dark"; const isDarkTheme = this.state.theme === THEME.DARK;
let frameIndex = 0; let frameIndex = 0;
let magicFrameIndex = 0; let magicFrameIndex = 0;
@ -2730,7 +2730,7 @@ class App extends React.Component<AppProps, AppState> {
this.excalidrawContainerRef.current?.classList.toggle( this.excalidrawContainerRef.current?.classList.toggle(
"theme--dark", "theme--dark",
this.state.theme === "dark", this.state.theme === THEME.DARK,
); );
if ( if (

@ -14,7 +14,9 @@ export const DarkModeToggle = (props: {
}) => { }) => {
const title = const title =
props.title || props.title ||
(props.value === "dark" ? t("buttons.lightMode") : t("buttons.darkMode")); (props.value === THEME.DARK
? t("buttons.lightMode")
: t("buttons.darkMode"));
return ( return (
<ToolButton <ToolButton

@ -3,7 +3,8 @@ import "./RadioGroup.scss";
export type RadioGroupChoice<T> = { export type RadioGroupChoice<T> = {
value: T; value: T;
label: string; label: React.ReactNode;
ariaLabel?: string;
}; };
export type RadioGroupProps<T> = { export type RadioGroupProps<T> = {
@ -26,13 +27,15 @@ export const RadioGroup = function <T>({
className={clsx("RadioGroup__choice", { className={clsx("RadioGroup__choice", {
active: choice.value === value, active: choice.value === value,
})} })}
key={choice.label} key={String(choice.value)}
title={choice.ariaLabel}
> >
<input <input
name={name} name={name}
type="radio" type="radio"
checked={choice.value === value} checked={choice.value === value}
onChange={() => onChange(choice.value)} onChange={() => onChange(choice.value)}
aria-label={choice.ariaLabel}
/> />
{choice.label} {choice.label}
</div> </div>

@ -75,6 +75,12 @@
&__shortcut { &__shortcut {
margin-inline-start: auto; margin-inline-start: auto;
opacity: 0.5; opacity: 0.5;
&--orphaned {
text-align: right;
font-size: 0.875rem;
padding: 0 0.625rem;
}
} }
&:hover { &: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 { .dropdown-menu-item-custom {
margin-top: 0.5rem; 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( export const SunIcon = createIcon(
<g <g stroke="currentColor" strokeLinejoin="round">
stroke="currentColor"
strokeWidth="1.25"
strokeLinecap="round"
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" /> <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>, </g>,
modifiedTablerIconProps, { ...modifiedTablerIconProps, strokeWidth: 1.5 },
); );
export const HamburgerMenuIcon = createIcon( export const HamburgerMenuIcon = createIcon(
@ -2092,3 +2087,11 @@ export const coffeeIcon = createIcon(
</g>, </g>,
tablerIconProps, 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"; } from "../App";
import { import {
boltIcon, boltIcon,
DeviceDesktopIcon,
ExportIcon, ExportIcon,
ExportImageIcon, ExportImageIcon,
HelpIcon, HelpIcon,
@ -35,6 +36,9 @@ import { jotaiScope } from "../../jotai";
import { useUIAppState } from "../../context/ui-appState"; import { useUIAppState } from "../../context/ui-appState";
import { openConfirmModal } from "../OverwriteConfirm/OverwriteConfirmState"; import { openConfirmModal } from "../OverwriteConfirm/OverwriteConfirmState";
import Trans from "../Trans"; import Trans from "../Trans";
import DropdownMenuItemContentRadio from "../dropdownMenu/DropdownMenuItemContentRadio";
import { THEME } from "../../constants";
import type { Theme } from "../../element/types";
import "./DefaultItems.scss"; import "./DefaultItems.scss";
@ -181,32 +185,80 @@ export const ClearCanvas = () => {
}; };
ClearCanvas.displayName = "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 { t } = useI18n();
const appState = useUIAppState(); const appState = useUIAppState();
const actionManager = useExcalidrawActionManager(); const actionManager = useExcalidrawActionManager();
const shortcut = getShortcutFromShortcutName("toggleTheme");
if (!actionManager.isActionEnabled(actionToggleTheme)) { if (!actionManager.isActionEnabled(actionToggleTheme)) {
return null; 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 ( return (
<DropdownMenuItem <DropdownMenuItem
onSelect={(event) => { onSelect={(event) => {
// do not close the menu when changing theme // do not close the menu when changing theme
event.preventDefault(); event.preventDefault();
if (props?.onSelect) {
props.onSelect(
appState.theme === THEME.DARK ? THEME.LIGHT : THEME.DARK,
);
} else {
return actionManager.executeAction(actionToggleTheme); return actionManager.executeAction(actionToggleTheme);
}
}} }}
icon={appState.theme === "dark" ? SunIcon : MoonIcon} icon={appState.theme === THEME.DARK ? SunIcon : MoonIcon}
data-testid="toggle-dark-mode" data-testid="toggle-dark-mode"
shortcut={getShortcutFromShortcutName("toggleTheme")} shortcut={shortcut}
aria-label={ aria-label={
appState.theme === "dark" appState.theme === THEME.DARK
? t("buttons.lightMode") ? t("buttons.lightMode")
: t("buttons.darkMode") : t("buttons.darkMode")
} }
> >
{appState.theme === "dark" {appState.theme === THEME.DARK
? t("buttons.lightMode") ? t("buttons.lightMode")
: t("buttons.darkMode")} : t("buttons.darkMode")}
</DropdownMenuItem> </DropdownMenuItem>

@ -1,3 +1,4 @@
import { THEME } from "../constants";
import { Theme } from "../element/types"; import { Theme } from "../element/types";
import { DataURL } from "../types"; import { DataURL } from "../types";
import { OpenAIInput, OpenAIOutput } from "./ai/types"; import { OpenAIInput, OpenAIOutput } from "./ai/types";
@ -39,7 +40,7 @@ export async function diagramToHTML({
image, image,
apiKey, apiKey,
text, text,
theme = "light", theme = THEME.LIGHT,
}: { }: {
image: DataURL; image: DataURL;
apiKey: string; apiKey: string;

@ -1,5 +1,6 @@
import { useState, useLayoutEffect } from "react"; import { useState, useLayoutEffect } from "react";
import { useDevice, useExcalidrawContainer } from "../components/App"; import { useDevice, useExcalidrawContainer } from "../components/App";
import { THEME } from "../constants";
import { useUIAppState } from "../context/ui-appState"; import { useUIAppState } from "../context/ui-appState";
export const useCreatePortalContainer = (opts?: { export const useCreatePortalContainer = (opts?: {
@ -18,7 +19,7 @@ export const useCreatePortalContainer = (opts?: {
div.className = ""; div.className = "";
div.classList.add("excalidraw", ...(opts?.className?.split(/\s+/) || [])); div.classList.add("excalidraw", ...(opts?.className?.split(/\s+/) || []));
div.classList.toggle("excalidraw--mobile", device.editor.isMobile); 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]); }, [div, theme, device.editor.isMobile, opts?.className]);

@ -110,6 +110,7 @@
"showStroke": "Show stroke color picker", "showStroke": "Show stroke color picker",
"showBackground": "Show background color picker", "showBackground": "Show background color picker",
"toggleTheme": "Toggle light/dark theme", "toggleTheme": "Toggle light/dark theme",
"theme": "Theme",
"personalLib": "Personal Library", "personalLib": "Personal Library",
"excalidrawLib": "Excalidraw Library", "excalidrawLib": "Excalidraw Library",
"decreaseFontSize": "Decrease font size", "decreaseFontSize": "Decrease font size",
@ -180,6 +181,7 @@
"fullScreen": "Full screen", "fullScreen": "Full screen",
"darkMode": "Dark mode", "darkMode": "Dark mode",
"lightMode": "Light mode", "lightMode": "Light mode",
"systemMode": "System mode",
"zenMode": "Zen mode", "zenMode": "Zen mode",
"objectsSnapMode": "Snap to objects", "objectsSnapMode": "Snap to objects",
"exitZenMode": "Exit zen mode", "exitZenMode": "Exit zen mode",

@ -2,7 +2,7 @@ import { StaticCanvasAppState, AppState } from "../types";
import { StaticCanvasRenderConfig } from "../scene/types"; import { StaticCanvasRenderConfig } from "../scene/types";
import { THEME_FILTER } from "../constants"; import { THEME, THEME_FILTER } from "../constants";
export const fillCircle = ( export const fillCircle = (
context: CanvasRenderingContext2D, context: CanvasRenderingContext2D,
@ -49,7 +49,7 @@ export const bootstrapCanvas = ({
context.setTransform(1, 0, 0, 1, 0, 0); context.setTransform(1, 0, 0, 1, 0, 0);
context.scale(scale, scale); context.scale(scale, scale);
if (isExporting && theme === "dark") { if (isExporting && theme === THEME.DARK) {
context.filter = THEME_FILTER; context.filter = THEME_FILTER;
} }

@ -41,6 +41,7 @@ import {
ELEMENT_READY_TO_ERASE_OPACITY, ELEMENT_READY_TO_ERASE_OPACITY,
FRAME_STYLE, FRAME_STYLE,
MIME_TYPES, MIME_TYPES,
THEME,
} from "../constants"; } from "../constants";
import { getStroke, StrokeOptions } from "perfect-freehand"; import { getStroke, StrokeOptions } from "perfect-freehand";
import { import {
@ -79,7 +80,7 @@ const shouldResetImageFilter = (
appState: StaticCanvasAppState, appState: StaticCanvasAppState,
) => { ) => {
return ( return (
appState.theme === "dark" && appState.theme === THEME.DARK &&
isInitializedImageElement(element) && isInitializedImageElement(element) &&
!isPendingImageElement(element, renderConfig) && !isPendingImageElement(element, renderConfig) &&
renderConfig.imageCache.get(element.fileId)?.mimeType !== MIME_TYPES.svg renderConfig.imageCache.get(element.fileId)?.mimeType !== MIME_TYPES.svg
@ -668,7 +669,7 @@ export const renderElement = (
// TODO change later to only affect AI frames // TODO change later to only affect AI frames
if (isMagicFrameElement(element)) { if (isMagicFrameElement(element)) {
context.strokeStyle = context.strokeStyle =
appState.theme === "light" ? "#7affd7" : "#1d8264"; appState.theme === THEME.LIGHT ? "#7affd7" : "#1d8264";
} }
if (FRAME_STYLE.radius && context.roundRect) { if (FRAME_STYLE.radius && context.roundRect) {

@ -1,3 +1,4 @@
import { THEME } from "../constants";
import { PointSnapLine, PointerSnapLine } from "../snapping"; import { PointSnapLine, PointerSnapLine } from "../snapping";
import { InteractiveCanvasAppState, Point } from "../types"; 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 // Don't change if zen mode, because we draw only crosses, we want the
// colors to be more visible // colors to be more visible
const snapColor = const snapColor =
appState.theme === "light" || appState.zenModeEnabled appState.theme === THEME.LIGHT || appState.zenModeEnabled
? SNAP_COLOR_LIGHT ? SNAP_COLOR_LIGHT
: SNAP_COLOR_DARK; : SNAP_COLOR_DARK;
// in zen mode make the cross more visible since we don't draw the lines // in zen mode make the cross more visible since we don't draw the lines

@ -19,6 +19,7 @@ import {
FONT_FAMILY, FONT_FAMILY,
FRAME_STYLE, FRAME_STYLE,
SVG_NS, SVG_NS,
THEME,
THEME_FILTER, THEME_FILTER,
} from "../constants"; } from "../constants";
import { getDefaultAppState } from "../appState"; import { getDefaultAppState } from "../appState";
@ -237,7 +238,7 @@ export const exportToCanvas = async (
scrollY: -minY + exportPadding, scrollY: -minY + exportPadding,
zoom: defaultAppState.zoom, zoom: defaultAppState.zoom,
shouldCacheIgnoreZoom: false, shouldCacheIgnoreZoom: false,
theme: appState.exportWithDarkMode ? "dark" : "light", theme: appState.exportWithDarkMode ? THEME.DARK : THEME.LIGHT,
}, },
renderConfig: { renderConfig: {
canvasBackgroundColor: viewBackgroundColor, canvasBackgroundColor: viewBackgroundColor,

@ -11,6 +11,20 @@ require("fake-indexeddb/auto");
polyfill(); 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", () => { vi.mock("nanoid", () => {
return { return {
nanoid: vi.fn(() => "test-id"), nanoid: vi.fn(() => "test-id"),