From 463857ad9ab225c16530a2a565cfc42525efc8ad Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Thu, 14 Oct 2021 14:15:57 +0530 Subject: [PATCH] feat: Export THEME from the package (#4055) * Use Theme type everywhere * Rename Appearance type to Theme for consistency * Reorder headers in readme The host don't need to pass hardcoded strings any more and instead can use the exported constant --- src/actions/actionCanvas.tsx | 5 +- src/actions/actionExport.tsx | 11 +- src/appState.ts | 3 +- src/components/App.tsx | 3 +- src/components/DarkModeToggle.tsx | 14 +- src/components/Modal.tsx | 3 +- src/components/icons.tsx | 826 +++++++++--------- src/constants.ts | 5 + src/element/types.ts | 3 +- .../components/GitHubCorner.tsx | 10 +- src/packages/excalidraw/CHANGELOG.md | 14 + src/packages/excalidraw/README_NEXT.md | 231 ++--- src/packages/excalidraw/index.tsx | 2 +- src/tests/packages/excalidraw.test.tsx | 6 +- src/types.ts | 5 +- 15 files changed, 582 insertions(+), 559 deletions(-) diff --git a/src/actions/actionCanvas.tsx b/src/actions/actionCanvas.tsx index eb811b62..4d01145d 100644 --- a/src/actions/actionCanvas.tsx +++ b/src/actions/actionCanvas.tsx @@ -4,7 +4,7 @@ import { ColorPicker } from "../components/ColorPicker"; import { trash, zoomIn, zoomOut } from "../components/icons"; import { ToolButton } from "../components/ToolButton"; import { DarkModeToggle } from "../components/DarkModeToggle"; -import { ZOOM_STEP } from "../constants"; +import { THEME, ZOOM_STEP } from "../constants"; import { getCommonBounds, getNonDeletedElements } from "../element"; import { newElementWith } from "../element/mutateElement"; import { ExcalidrawElement } from "../element/types"; @@ -279,7 +279,8 @@ export const actionToggleTheme = register({ return { appState: { ...appState, - theme: value || (appState.theme === "light" ? "dark" : "light"), + theme: + value || (appState.theme === THEME.LIGHT ? THEME.DARK : THEME.LIGHT), }, commitToHistory: false, }; diff --git a/src/actions/actionExport.tsx b/src/actions/actionExport.tsx index 605e0878..41fc14f1 100644 --- a/src/actions/actionExport.tsx +++ b/src/actions/actionExport.tsx @@ -5,7 +5,7 @@ import { ProjectName } from "../components/ProjectName"; import { ToolButton } from "../components/ToolButton"; import "../components/ToolIcon.scss"; import { Tooltip } from "../components/Tooltip"; -import { DarkModeToggle, Appearence } from "../components/DarkModeToggle"; +import { DarkModeToggle } from "../components/DarkModeToggle"; import { loadFromJSON, saveAsJSON } from "../data"; import { resaveAsImageWithScene } from "../data/resave"; import { t } from "../i18n"; @@ -14,12 +14,13 @@ import { KEYS } from "../keys"; import { register } from "./register"; import { CheckboxItem } from "../components/CheckboxItem"; import { getExportSize } from "../scene/export"; -import { DEFAULT_EXPORT_PADDING, EXPORT_SCALES } from "../constants"; +import { DEFAULT_EXPORT_PADDING, EXPORT_SCALES, THEME } from "../constants"; import { getSelectedElements, isSomeElementSelected } from "../scene"; import { getNonDeletedElements } from "../element"; import { ActiveFile } from "../components/ActiveFile"; import { isImageFileHandle } from "../data/blob"; import { nativeFileSystemSupported } from "../data/filesystem"; +import { Theme } from "../element/types"; export const actionChangeProjectName = register({ name: "changeProjectName", @@ -256,9 +257,9 @@ export const actionExportWithDarkMode = register({ }} > { - updateData(theme === "dark"); + value={appState.exportWithDarkMode ? THEME.DARK : THEME.LIGHT} + onChange={(theme: Theme) => { + updateData(theme === THEME.DARK); }} title={t("labels.toggleExportColorScheme")} /> diff --git a/src/appState.ts b/src/appState.ts index 88e293f0..42e708d2 100644 --- a/src/appState.ts +++ b/src/appState.ts @@ -4,6 +4,7 @@ import { DEFAULT_FONT_SIZE, DEFAULT_TEXT_ALIGN, EXPORT_SCALES, + THEME, } from "./constants"; import { t } from "./i18n"; import { AppState, NormalizedZoomValue } from "./types"; @@ -18,7 +19,7 @@ export const getDefaultAppState = (): Omit< "offsetTop" | "offsetLeft" | "width" | "height" > => { return { - theme: "light", + theme: THEME.LIGHT, collaborators: new Map(), currentChartType: "bar", currentItemBackgroundColor: "transparent", diff --git a/src/components/App.tsx b/src/components/App.tsx index 41f4f43b..d5564c20 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -60,6 +60,7 @@ import { SCROLL_TIMEOUT, TAP_TWICE_TIMEOUT, TEXT_TO_CENTER_SNAP_THRESHOLD, + THEME, TOUCH_CTX_MENU_TIMEOUT, URL_HASH_KEYS, URL_QUERY_KEYS, @@ -513,7 +514,7 @@ class App extends React.Component { let viewModeEnabled = actionResult?.appState?.viewModeEnabled || false; let zenModeEnabled = actionResult?.appState?.zenModeEnabled || false; let gridSize = actionResult?.appState?.gridSize || null; - let theme = actionResult?.appState?.theme || "light"; + let theme = actionResult?.appState?.theme || THEME.LIGHT; let name = actionResult?.appState?.name ?? this.state.name; if (typeof this.props.viewModeEnabled !== "undefined") { viewModeEnabled = this.props.viewModeEnabled; diff --git a/src/components/DarkModeToggle.tsx b/src/components/DarkModeToggle.tsx index a22c9a14..44397ce9 100644 --- a/src/components/DarkModeToggle.tsx +++ b/src/components/DarkModeToggle.tsx @@ -3,14 +3,14 @@ import "./ToolIcon.scss"; import React from "react"; import { t } from "../i18n"; import { ToolButton } from "./ToolButton"; - -export type Appearence = "light" | "dark"; +import { THEME } from "../constants"; +import { Theme } from "../element/types"; // We chose to use only explicit toggle and not a third option for system value, // but this could be added in the future. export const DarkModeToggle = (props: { - value: Appearence; - onChange: (value: Appearence) => void; + value: Theme; + onChange: (value: Theme) => void; title?: string; }) => { const title = @@ -20,10 +20,12 @@ export const DarkModeToggle = (props: { return ( props.onChange(props.value === "dark" ? "light" : "dark")} + onClick={() => + props.onChange(props.value === THEME.DARK ? THEME.LIGHT : THEME.DARK) + } data-testid="toggle-dark-mode" /> ); diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx index bd61f2d3..c78b4eb7 100644 --- a/src/components/Modal.tsx +++ b/src/components/Modal.tsx @@ -6,6 +6,7 @@ import clsx from "clsx"; import { KEYS } from "../keys"; import { useExcalidrawContainer, useIsMobile } from "./App"; import { AppState } from "../types"; +import { THEME } from "../constants"; export const Modal = (props: { className?: string; @@ -15,7 +16,7 @@ export const Modal = (props: { labelledBy: string; theme?: AppState["theme"]; }) => { - const { theme = "light" } = props; + const { theme = THEME.LIGHT } = props; const modalRoot = useBodyRoot(theme); if (!modalRoot) { diff --git a/src/components/icons.tsx b/src/components/icons.tsx index c64fd925..fb1f5772 100644 --- a/src/components/icons.tsx +++ b/src/components/icons.tsx @@ -10,13 +10,15 @@ import React from "react"; import oc from "open-color"; import clsx from "clsx"; +import { Theme } from "../element/types"; +import { THEME } from "../constants"; -const activeElementColor = (theme: "light" | "dark") => - theme === "light" ? oc.orange[4] : oc.orange[9]; -const iconFillColor = (theme: "light" | "dark") => - theme === "light" ? oc.black : oc.gray[4]; -const handlerColor = (theme: "light" | "dark") => - theme === "light" ? oc.white : "#1e1e1e"; +const activeElementColor = (theme: Theme) => + theme === THEME.LIGHT ? oc.orange[4] : oc.orange[9]; +const iconFillColor = (theme: Theme) => + theme === THEME.LIGHT ? oc.black : oc.gray[4]; +const handlerColor = (theme: Theme) => + theme === THEME.LIGHT ? oc.white : "#1e1e1e"; type Opts = { width?: number; @@ -175,88 +177,84 @@ export const resetZoom = createIcon( { width: 1024 }, ); -export const BringForwardIcon = React.memo( - ({ theme }: { theme: "light" | "dark" }) => - createIcon( - <> - - - , - { width: 24, mirror: true }, - ), +export const BringForwardIcon = React.memo(({ theme }: { theme: Theme }) => + createIcon( + <> + + + , + { width: 24, mirror: true }, + ), ); -export const SendBackwardIcon = React.memo( - ({ theme }: { theme: "light" | "dark" }) => - createIcon( - <> - - - , - { width: 24, mirror: true }, - ), +export const SendBackwardIcon = React.memo(({ theme }: { theme: Theme }) => + createIcon( + <> + + + , + { width: 24, mirror: true }, + ), ); -export const BringToFrontIcon = React.memo( - ({ theme }: { theme: "light" | "dark" }) => - createIcon( - <> - - - , - { width: 24, mirror: true }, - ), +export const BringToFrontIcon = React.memo(({ theme }: { theme: Theme }) => + createIcon( + <> + + + , + { width: 24, mirror: true }, + ), ); -export const SendToBackIcon = React.memo( - ({ theme }: { theme: "light" | "dark" }) => - createIcon( - <> - - - , - { width: 24, mirror: true }, - ), +export const SendToBackIcon = React.memo(({ theme }: { theme: Theme }) => + createIcon( + <> + + + , + { width: 24, mirror: true }, + ), ); // @@ -265,96 +263,92 @@ export const SendToBackIcon = React.memo( // first one the user sees. Horizontal align icons should not be flipped since // that would make them lie about their function. // -export const AlignTopIcon = React.memo( - ({ theme }: { theme: "light" | "dark" }) => - createIcon( - <> - - - , - { width: 24, mirror: true }, - ), +export const AlignTopIcon = React.memo(({ theme }: { theme: Theme }) => + createIcon( + <> + + + , + { width: 24, mirror: true }, + ), ); -export const AlignBottomIcon = React.memo( - ({ theme }: { theme: "light" | "dark" }) => - createIcon( - <> - - - , - { width: 24, mirror: true }, - ), +export const AlignBottomIcon = React.memo(({ theme }: { theme: Theme }) => + createIcon( + <> + + + , + { width: 24, mirror: true }, + ), ); -export const AlignLeftIcon = React.memo( - ({ theme }: { theme: "light" | "dark" }) => - createIcon( - <> - - - , - { width: 24 }, - ), +export const AlignLeftIcon = React.memo(({ theme }: { theme: Theme }) => + createIcon( + <> + + + , + { width: 24 }, + ), ); -export const AlignRightIcon = React.memo( - ({ theme }: { theme: "light" | "dark" }) => - createIcon( - <> - - - , - { width: 24 }, - ), +export const AlignRightIcon = React.memo(({ theme }: { theme: Theme }) => + createIcon( + <> + + + , + { width: 24 }, + ), ); export const DistributeHorizontallyIcon = React.memo( - ({ theme }: { theme: "light" | "dark" }) => + ({ theme }: { theme: Theme }) => createIcon( <> + ({ theme }: { theme: Theme }) => createIcon( <> - createIcon( - <> - - - , - { width: 24, mirror: true }, - ), +export const CenterVerticallyIcon = React.memo(({ theme }: { theme: Theme }) => + createIcon( + <> + + + , + { width: 24, mirror: true }, + ), ); export const CenterHorizontallyIcon = React.memo( - ({ theme }: { theme: "light" | "dark" }) => + ({ theme }: { theme: Theme }) => createIcon( <> +export const GroupIcon = React.memo(({ theme }: { theme: Theme }) => createIcon( <> @@ -512,73 +505,69 @@ export const GroupIcon = React.memo(({ theme }: { theme: "light" | "dark" }) => ), ); -export const UngroupIcon = React.memo( - ({ theme }: { theme: "light" | "dark" }) => - createIcon( - <> - - - - - - - - - - - - - , - { width: 182, height: 182, mirror: true }, - ), -); - -export const FillHachureIcon = React.memo( - ({ theme }: { theme: "light" | "dark" }) => - createIcon( +export const UngroupIcon = React.memo(({ theme }: { theme: Theme }) => + createIcon( + <> + , - { width: 40, height: 20 }, - ), + d="M25 111C25 80.2068 25 49.4135 25 26M25 26C48.6174 26 72.2348 26 111 26H25ZM25 26C53.3671 26 81.7343 26 111 26H25ZM111 26C111 52.303 111 78.606 111 111V26ZM111 26C111 51.2947 111 76.5893 111 111V26ZM111 111C87.0792 111 63.1585 111 25 111H111ZM111 111C87.4646 111 63.9293 111 25 111H111ZM25 111C25 81.1514 25 51.3028 25 26V111Z" + stroke={iconFillColor(theme)} + strokeWidth="2" + /> + + + + + + + + + + + , + { width: 182, height: 182, mirror: true }, + ), ); -export const FillCrossHatchIcon = React.memo( - ({ theme }: { theme: "light" | "dark" }) => - createIcon( - - - - , - { width: 40, height: 20 }, - ), +export const FillHachureIcon = React.memo(({ theme }: { theme: Theme }) => + createIcon( + , + { width: 40, height: 20 }, + ), ); -export const FillSolidIcon = React.memo( - ({ theme }: { theme: "light" | "dark" }) => - createIcon(, { - width: 40, - height: 20, - }), +export const FillCrossHatchIcon = React.memo(({ theme }: { theme: Theme }) => + createIcon( + + + + , + { width: 40, height: 20 }, + ), +); + +export const FillSolidIcon = React.memo(({ theme }: { theme: Theme }) => + createIcon(, { + width: 40, + height: 20, + }), ); export const StrokeWidthIcon = React.memo( - ({ theme, strokeWidth }: { theme: "light" | "dark"; strokeWidth: number }) => + ({ theme, strokeWidth }: { theme: Theme; strokeWidth: number }) => createIcon( - createIcon( - , - { - width: 40, - height: 20, - }, - ), +export const StrokeStyleSolidIcon = React.memo(({ theme }: { theme: Theme }) => + createIcon( + , + { + width: 40, + height: 20, + }, + ), ); -export const StrokeStyleDashedIcon = React.memo( - ({ theme }: { theme: "light" | "dark" }) => - createIcon( - , - { width: 40, height: 20 }, - ), +export const StrokeStyleDashedIcon = React.memo(({ theme }: { theme: Theme }) => + createIcon( + , + { width: 40, height: 20 }, + ), ); -export const StrokeStyleDottedIcon = React.memo( - ({ theme }: { theme: "light" | "dark" }) => - createIcon( - , - { width: 40, height: 20 }, - ), +export const StrokeStyleDottedIcon = React.memo(({ theme }: { theme: Theme }) => + createIcon( + , + { width: 40, height: 20 }, + ), ); export const SloppinessArchitectIcon = React.memo( - ({ theme }: { theme: "light" | "dark" }) => + ({ theme }: { theme: Theme }) => createIcon( - createIcon( - , - { width: 40, height: 20, mirror: true }, - ), +export const SloppinessArtistIcon = React.memo(({ theme }: { theme: Theme }) => + createIcon( + , + { width: 40, height: 20, mirror: true }, + ), ); export const SloppinessCartoonistIcon = React.memo( - ({ theme }: { theme: "light" | "dark" }) => + ({ theme }: { theme: Theme }) => createIcon( - createIcon( - , - { width: 40, height: 20, mirror: true }, - ), +export const EdgeSharpIcon = React.memo(({ theme }: { theme: Theme }) => + createIcon( + , + { width: 40, height: 20, mirror: true }, + ), ); -export const EdgeRoundIcon = React.memo( - ({ theme }: { theme: "light" | "dark" }) => - createIcon( - , - { width: 40, height: 20, mirror: true }, - ), +export const EdgeRoundIcon = React.memo(({ theme }: { theme: Theme }) => + createIcon( + , + { width: 40, height: 20, mirror: true }, + ), ); -export const ArrowheadNoneIcon = React.memo( - ({ theme }: { theme: "light" | "dark" }) => - createIcon( - , - { - width: 40, - height: 20, - }, - ), +export const ArrowheadNoneIcon = React.memo(({ theme }: { theme: Theme }) => + createIcon( + , + { + width: 40, + height: 20, + }, + ), ); export const ArrowheadArrowIcon = React.memo( - ({ theme, flip = false }: { theme: "light" | "dark"; flip?: boolean }) => + ({ theme, flip = false }: { theme: Theme; flip?: boolean }) => createIcon( + ({ theme, flip = false }: { theme: Theme; flip?: boolean }) => createIcon( + ({ theme, flip = false }: { theme: Theme; flip?: boolean }) => createIcon( - createIcon( - , - { width: 47, height: 77 }, - ), +export const FontSizeSmallIcon = React.memo(({ theme }: { theme: Theme }) => + createIcon( + , + { width: 47, height: 77 }, + ), ); -export const FontSizeMediumIcon = React.memo( - ({ theme }: { theme: "light" | "dark" }) => - createIcon( - , - { width: 77, height: 75 }, - ), +export const FontSizeMediumIcon = React.memo(({ theme }: { theme: Theme }) => + createIcon( + , + { width: 77, height: 75 }, + ), ); -export const FontSizeLargeIcon = React.memo( - ({ theme }: { theme: "light" | "dark" }) => - createIcon( - , - { width: 45, height: 75 }, - ), +export const FontSizeLargeIcon = React.memo(({ theme }: { theme: Theme }) => + createIcon( + , + { width: 45, height: 75 }, + ), ); export const FontSizeExtraLargeIcon = React.memo( - ({ theme }: { theme: "light" | "dark" }) => + ({ theme }: { theme: Theme }) => createIcon( + ({ theme }: { theme: Theme }) => createIcon( - createIcon( - <> - - - , - { width: 70, height: 78 }, - ), -); - -export const FontFamilyCodeIcon = React.memo( - ({ theme }: { theme: "light" | "dark" }) => - createIcon( - <> - - , - { width: 640, height: 512 }, - ), -); - -export const TextAlignLeftIcon = React.memo( - ({ theme }: { theme: "light" | "dark" }) => - createIcon( +export const FontFamilyNormalIcon = React.memo(({ theme }: { theme: Theme }) => + createIcon( + <> , - { width: 448, height: 512 }, - ), + d="M 63.818 71.68 L 54.492 71.68 L 45.898 49.561 L 17.578 49.561 L 9.082 71.68 L 0 71.68 L 27.881 0 L 35.986 0 L 63.818 71.68 Z M 20.605 41.602 L 43.213 41.602 L 35.205 19.971 L 31.787 9.277 Q 30.322 15.137 28.711 19.971 L 20.605 41.602 Z" + /> + + , + { width: 70, height: 78 }, + ), ); -export const TextAlignCenterIcon = React.memo( - ({ theme }: { theme: "light" | "dark" }) => - createIcon( +export const FontFamilyCodeIcon = React.memo(({ theme }: { theme: Theme }) => + createIcon( + <> , - { width: 448, height: 512 }, - ), + d="M278.9 511.5l-61-17.7c-6.4-1.8-10-8.5-8.2-14.9L346.2 8.7c1.8-6.4 8.5-10 14.9-8.2l61 17.7c6.4 1.8 10 8.5 8.2 14.9L293.8 503.3c-1.9 6.4-8.5 10.1-14.9 8.2zm-114-112.2l43.5-46.4c4.6-4.9 4.3-12.7-.8-17.2L117 256l90.6-79.7c5.1-4.5 5.5-12.3.8-17.2l-43.5-46.4c-4.5-4.8-12.1-5.1-17-.5L3.8 247.2c-5.1 4.7-5.1 12.8 0 17.5l144.1 135.1c4.9 4.6 12.5 4.4 17-.5zm327.2.6l144.1-135.1c5.1-4.7 5.1-12.8 0-17.5L492.1 112.1c-4.8-4.5-12.4-4.3-17 .5L431.6 159c-4.6 4.9-4.3 12.7.8 17.2L523 256l-90.6 79.7c-5.1 4.5-5.5 12.3-.8 17.2l43.5 46.4c4.5 4.9 12.1 5.1 17 .6z" + /> + , + { width: 640, height: 512 }, + ), ); -export const TextAlignRightIcon = React.memo( - ({ theme }: { theme: "light" | "dark" }) => - createIcon( - , - { width: 448, height: 512 }, - ), +export const TextAlignLeftIcon = React.memo(({ theme }: { theme: Theme }) => + createIcon( + , + { width: 448, height: 512 }, + ), +); + +export const TextAlignCenterIcon = React.memo(({ theme }: { theme: Theme }) => + createIcon( + , + { width: 448, height: 512 }, + ), +); + +export const TextAlignRightIcon = React.memo(({ theme }: { theme: Theme }) => + createIcon( + , + { width: 448, height: 512 }, + ), ); diff --git a/src/constants.ts b/src/constants.ts index 57d95aeb..0e18472d 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -70,6 +70,11 @@ export const FONT_FAMILY = { Cascadia: 3, }; +export const THEME = { + LIGHT: "light", + DARK: "dark", +}; + export const WINDOWS_EMOJI_FALLBACK_FONT = "Segoe UI Emoji"; export const DEFAULT_FONT_SIZE = 20; diff --git a/src/element/types.ts b/src/element/types.ts index 34533e68..83f06975 100644 --- a/src/element/types.ts +++ b/src/element/types.ts @@ -1,10 +1,11 @@ import { Point } from "../types"; -import { FONT_FAMILY } from "../constants"; +import { FONT_FAMILY, THEME } from "../constants"; export type ChartType = "bar" | "line"; export type FillStyle = "hachure" | "cross-hatch" | "solid"; export type FontFamilyKeys = keyof typeof FONT_FAMILY; export type FontFamilyValues = typeof FONT_FAMILY[FontFamilyKeys]; +export type Theme = typeof THEME[keyof typeof THEME]; export type FontString = string & { _brand: "fontString" }; export type GroupId = string; export type PointerType = "mouse" | "pen" | "touch"; diff --git a/src/excalidraw-app/components/GitHubCorner.tsx b/src/excalidraw-app/components/GitHubCorner.tsx index cc8a06d5..5e08590f 100644 --- a/src/excalidraw-app/components/GitHubCorner.tsx +++ b/src/excalidraw-app/components/GitHubCorner.tsx @@ -1,9 +1,11 @@ import oc from "open-color"; import React from "react"; +import { THEME } from "../../constants"; +import { Theme } from "../../element/types"; // https://github.com/tholman/github-corners export const GitHubCorner = React.memo( - ({ theme, dir }: { theme: "light" | "dark"; dir: string }) => ( + ({ theme, dir }: { theme: Theme; dir: string }) => ( diff --git a/src/packages/excalidraw/CHANGELOG.md b/src/packages/excalidraw/CHANGELOG.md index bdc2a76b..338204c1 100644 --- a/src/packages/excalidraw/CHANGELOG.md +++ b/src/packages/excalidraw/CHANGELOG.md @@ -11,6 +11,20 @@ The change should be grouped under one of the below section and must contain PR Please add the latest change on the top under the correct section. --> +## Unreleased + +## Excalidraw API + +### Features + +- Export `THEME` constant from the package so host can use this when passing the theme + + #### BREAKING CHANGE + + The `Appearance` type is now removed and renamed to `Theme` so `Theme` type needs to be used. + +--- + ## 0.10.0 (2021-10-13) ## Excalidraw API diff --git a/src/packages/excalidraw/README_NEXT.md b/src/packages/excalidraw/README_NEXT.md index 64b81a51..0130645a 100644 --- a/src/packages/excalidraw/README_NEXT.md +++ b/src/packages/excalidraw/README_NEXT.md @@ -371,7 +371,7 @@ To view the full example visit :point_down: | [`zenModeEnabled`](#zenModeEnabled) | boolean | | This implies if the zen mode is enabled | | [`gridModeEnabled`](#gridModeEnabled) | boolean | | This implies if the grid mode is enabled | | [`libraryReturnUrl`](#libraryReturnUrl) | string | | What URL should [libraries.excalidraw.com](https://libraries.excalidraw.com) be installed to | -| [`theme`](#theme) | `light` or `dark` | | The theme of the Excalidraw component | +| [`theme`](#theme) | [THEME.LIGHT](#THEME-1) | [THEME.LIGHT](#THEME-1) | [THEME.LIGHT](#THEME-1) | The theme of the Excalidraw component | | [`name`](#name) | string | | Name of the drawing | | [`UIOptions`](#UIOptions) |
{ canvasActions:  CanvasActions }
| [DEFAULT UI OPTIONS](https://github.com/excalidraw/excalidraw/blob/master/src/constants.ts#L129) | To customise UI options. Currently we support customising [`canvas actions`](#canvasActions) | | [`onPaste`](#onPaste) |
(data: ClipboardData, event: ClipboardEvent | null) => boolean
| | Callback to be triggered if passed when the something is pasted in to the scene | @@ -564,7 +564,7 @@ If supplied, this URL will be used when user tries to install a library from [li #### `theme` -This prop controls Excalidraw's theme. When supplied, the value takes precedence over `intialData.appState.theme`, the theme will be fully controlled by the host app, and users won't be able to toggle it from within the app. +This prop controls Excalidraw's theme. When supplied, the value takes precedence over `intialData.appState.theme`, the theme will be fully controlled by the host app, and users won't be able to toggle it from within the app. You can use [`THEME`](#THEME-1) to specify the theme. #### `name` @@ -612,11 +612,7 @@ This callback must return a `boolean` value or a [promise](https://developer.moz In case you want to prevent the excalidraw paste action you must return `false`, it will stop the native excalidraw clipboard management flow (nothing will be pasted into the scene). -### Does it support collaboration ? - -No, Excalidraw package doesn't come with collaboration built in, since the implementation is specific to each host app. We expose APIs which you can use to communicate with Excalidraw which you can use to implement it. You can check our own implementation [here](https://github.com/excalidraw/excalidraw/blob/master/src/excalidraw-app/index.tsx). - -### importLibrary +#### `importLibrary` Imports library from given URL. You should call this on `hashchange`, passing the `addLibrary` value if you detect it as shown below. Optionally pass a CSRF `token` to skip prompting during installation (retrievable via `token` key from the url coming from [https://libraries.excalidraw.com](https://libraries.excalidraw.com/)). @@ -638,17 +634,17 @@ useEffect(() => { Try out the [Demo](#Demo) to see it in action. -### detectScroll +#### `detectScroll` Indicates whether Excalidraw should listen for `scroll` event on the nearest scrollable container in the DOM tree and recompute the coordinates (e.g. to correctly handle the cursor) when the component's position changes. You can disable this when you either know this doesn't affect your app or you want to take care of it yourself (calling the [`refresh()`](#ref) method). -### handleKeyboardGlobally +#### `handleKeyboardGlobally` Indicates whether to bind keyboard events to `document`. Disabled by default, meaning the keyboard events are bound to the Excalidraw component. This allows for multiple Excalidraw components to live on the same page, and ensures that Excalidraw keyboard handling doesn't collide with your app's (or the browser) when the component isn't focused. Enable this if you want Excalidraw to handle keyboard even if the component isn't focused (e.g. a user is interacting with the navbar, sidebar, or similar). -### onLibraryChange +#### `onLibraryChange` Ths callback if supplied will get triggered when the library is updated and has the below signature. @@ -658,58 +654,17 @@ Ths callback if supplied will get triggered when the library is updated and has It is invoked with empty items when user clears the library. You can use this callback when you want to do something additional when library is updated for example persisting it to local storage. -### id +#### `id` The unique id of the excalidraw component. This can be used to identify the excalidraw component, for example importing the library items to the excalidraw component from where it was initiated when you have multiple excalidraw components rendered on the same page as shown in [multiple excalidraw demo](https://codesandbox.io/s/multiple-excalidraw-k1xx5). -### autoFocus +#### `autoFocus` This prop implies whether to focus the Excalidraw component on page load. Defaults to false. -### Extra API's +### Does it support collaboration ? -#### `getSceneVersion` - -**How to use** - -
-import { getSceneVersion } from "@excalidraw/excalidraw-next";
-getSceneVersion(elements:  ExcalidrawElement[])
-
- -This function returns the current scene version. - -#### `isInvisiblySmallElement` - -**_Signature_** - -
-isInvisiblySmallElement(element:  ExcalidrawElement): boolean
-
- -**How to use** - -```js -import { isInvisiblySmallElement } from "@excalidraw/excalidraw-next"; -``` - -Returns `true` if element is invisibly small (e.g. width & height are zero). - -#### `getElementMap` - -**_Signature_** - -
-getElementsMap(elements:  ExcalidrawElement[]): {[id: string]: ExcalidrawElement}
-
- -**How to use** - -```js -import { getElementsMap } from "@excalidraw/excalidraw-next"; -``` - -This function returns an object where each element is mapped to its id. +No, Excalidraw package doesn't come with collaboration built in, since the implementation is specific to each host app. We expose APIs which you can use to communicate with Excalidraw which you can use to implement it. You can check our own implementation [here](https://github.com/excalidraw/excalidraw/blob/master/src/excalidraw-app/index.tsx). ### Restore utilities @@ -767,19 +722,6 @@ import { restore } from "@excalidraw/excalidraw-next"; This function makes sure elements and state is set to appropriate values and set to default value if not present. It is a combination of [restoreElements](#restoreElements) and [restoreAppState](#restoreAppState). -#### `serializeAsJSON` - -**_Signature_** - -
-serializeAsJSON({
-  elements: ExcalidrawElement[],
-  appState: AppState,
-}): string
-
- -Takes the scene elements and state and returns a JSON string. Deleted `elements`as well as most properties from `AppState` are removed from the resulting JSON. (see [`serializeAsJSON()`](https://github.com/excalidraw/excalidraw/blob/master/src/data/json.ts#L16) source for details). - ### Export utilities #### `exportToCanvas` @@ -864,7 +806,113 @@ This function returns a promise which resolves to svg of the exported drawing. | exportWithDarkMode | boolean | false | Indicates whether to export with dark mode | | exportEmbedScene | boolean | false | Indicates whether scene data should be embedded in svg. This will increase the svg size. | -### FONT_FAMILY +### Extra API's + +#### `serializeAsJSON` + +**_Signature_** + +
+serializeAsJSON({
+  elements: ExcalidrawElement[],
+  appState: AppState,
+}): string
+
+ +Takes the scene elements and state and returns a JSON string. Deleted `elements`as well as most properties from `AppState` are removed from the resulting JSON. (see [`serializeAsJSON()`](https://github.com/excalidraw/excalidraw/blob/master/src/data/json.ts#L16) source for details). + +#### `getSceneVersion` + +**How to use** + +
+import { getSceneVersion } from "@excalidraw/excalidraw-next";
+getSceneVersion(elements:  ExcalidrawElement[])
+
+ +This function returns the current scene version. + +#### `isInvisiblySmallElement` + +**_Signature_** + +
+isInvisiblySmallElement(element:  ExcalidrawElement): boolean
+
+ +**How to use** + +```js +import { isInvisiblySmallElement } from "@excalidraw/excalidraw-next"; +``` + +Returns `true` if element is invisibly small (e.g. width & height are zero). + +#### `getElementMap` + +**_Signature_** + +
+getElementsMap(elements:  ExcalidrawElement[]): {[id: string]: ExcalidrawElement}
+
+ +**How to use** + +```js +import { getElementsMap } from "@excalidraw/excalidraw-next"; +``` + +This function returns an object where each element is mapped to its id. + +#### `loadLibraryFromBlob` + +```js +import { loadLibraryFromBlob } from "@excalidraw/excalidraw-next"; +``` + +**_Signature_** + +
+loadLibraryFromBlob(blob: Blob)
+
+ +This function loads the library from the blob. + +#### `loadFromBlob` + +**How to use** + +```js +import { loadFromBlob } from "@excalidraw/excalidraw-next"; +``` + +**Signature** + +
+loadFromBlob(blob: Blob, localAppState:  AppState | null)
+
+ +This function loads the scene data from the blob. If you pass `localAppState`, `localAppState` value will be preferred over the `appState` derived from `blob` + +#### `getFreeDrawSvgPath` + +**How to use** + +```js +import { getFreeDrawSvgPath } from "@excalidraw/excalidraw-next"; +``` + +**Signature** + +
+getFreeDrawSvgPath(element: ExcalidrawFreeDrawElement
+
+ +This function returns the free draw svg path for the element. + +### Exported constants + +#### `FONT_FAMILY` **How to use** @@ -882,51 +930,22 @@ import { FONT_FAMILY } from "@excalidraw/excalidraw-next"; Defaults to `FONT_FAMILY.Virgil` unless passed in `initialData.appState.currentItemFontFamily`. -### loadLibraryFromBlob - -```js -import { loadLibraryFromBlob } from "@excalidraw/excalidraw-next"; -``` - -**_Signature_** - -
-loadLibraryFromBlob(blob: Blob)
-
- -This function loads the library from the blob. - -### loadFromBlob +#### `THEME` **How to use** ```js -import { loadFromBlob } from "@excalidraw/excalidraw-next"; +import { THEME } from "@excalidraw/excalidraw-next"; ``` -**Signature** +`THEME` contains all the themes supported by `Excalidraw` as explained below -
-loadFromBlob(blob: Blob, localAppState:  AppState | null)
-
+| Theme | Description | +| ----- | --------------- | +| LIGHT | The light theme | +| DARK | The Dark theme | -This function loads the scene data from the blob. If you pass `localAppState`, `localAppState` value will be preferred over the `appState` derived from `blob` - -### getFreeDrawSvgPath - -**How to use** - -```js -import { getFreeDrawSvgPath } from "@excalidraw/excalidraw-next"; -``` - -**Signature** - -
-getFreeDrawSvgPath(element: ExcalidrawFreeDrawElement
-
- -This function returns the free draw svg path for the element. +Defaults to `THEME.LIGHT` unless passed in `initialData.appState.theme` ## Need help? diff --git a/src/packages/excalidraw/index.tsx b/src/packages/excalidraw/index.tsx index 1291bb0a..1d3c72a7 100644 --- a/src/packages/excalidraw/index.tsx +++ b/src/packages/excalidraw/index.tsx @@ -183,4 +183,4 @@ export { loadFromBlob, getFreeDrawSvgPath, } from "../../packages/utils"; -export { FONT_FAMILY } from "../../constants"; +export { FONT_FAMILY, THEME } from "../../constants"; diff --git a/src/tests/packages/excalidraw.test.tsx b/src/tests/packages/excalidraw.test.tsx index 947964cf..b6d1e8ca 100644 --- a/src/tests/packages/excalidraw.test.tsx +++ b/src/tests/packages/excalidraw.test.tsx @@ -2,7 +2,7 @@ import React from "react"; import { fireEvent, GlobalTestState, render } from "../test-utils"; import Excalidraw from "../../packages/excalidraw/index"; import { queryByText, queryByTestId } from "@testing-library/react"; -import { GRID_SIZE } from "../../constants"; +import { GRID_SIZE, THEME } from "../../constants"; import { t } from "../../i18n"; const { h } = window; @@ -91,7 +91,7 @@ describe("", () => { describe("Test theme prop", () => { it('should show the dark mode toggle when the theme prop is "undefined"', async () => { const { container } = await render(); - expect(h.state.theme).toBe("light"); + expect(h.state.theme).toBe(THEME.LIGHT); const darkModeToggle = queryByTestId(container, "toggle-dark-mode"); @@ -100,7 +100,7 @@ describe("", () => { it('should not show the dark mode toggle when the theme prop is not "undefined"', async () => { const { container } = await render(); - expect(h.state.theme).toBe("dark"); + expect(h.state.theme).toBe(THEME.DARK); expect(queryByTestId(container, "toggle-dark-mode")).toBe(null); }); diff --git a/src/types.ts b/src/types.ts index c595ddab..dfb8362a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -10,6 +10,7 @@ import { Arrowhead, ChartType, FontFamilyValues, + Theme, } from "./element/types"; import { SHAPES } from "./shapes"; import { Point as RoughPoint } from "roughjs/bin/geometry"; @@ -98,7 +99,7 @@ export type AppState = { showHelpDialog: boolean; toastMessage: string | null; zenModeEnabled: boolean; - theme: "light" | "dark"; + theme: Theme; gridSize: number | null; viewModeEnabled: boolean; @@ -192,7 +193,7 @@ export interface ExcalidrawProps { zenModeEnabled?: boolean; gridModeEnabled?: boolean; libraryReturnUrl?: string; - theme?: "dark" | "light"; + theme?: Theme; name?: string; renderCustomStats?: ( elements: readonly NonDeletedExcalidrawElement[],