mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-09-03 15:47:05 +02:00
Compare commits
8 Commits
arrow-fram
...
barnabasmo
Author | SHA1 | Date | |
---|---|---|---|
![]() |
502cee7b7a | ||
![]() |
e57dc405fa | ||
![]() |
41ed019bc2 | ||
![]() |
f7c3644342 | ||
![]() |
5e3550fc14 | ||
![]() |
70888327a3 | ||
![]() |
9fc15d81a0 | ||
![]() |
a80ac4c748 |
30
.github/workflows/size-limit.yml
vendored
Normal file
30
.github/workflows/size-limit.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
name: "Bundle Size check @excalidraw/excalidraw"
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
jobs:
|
||||
size:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CI_JOB_NUMBER: 1
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
- name: Setup Node.js 18.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18.x
|
||||
- name: Install
|
||||
run: yarn --frozen-lockfile
|
||||
- name: Install in src/packages/excalidraw
|
||||
run: yarn --frozen-lockfile
|
||||
working-directory: src/packages/excalidraw
|
||||
env:
|
||||
CI: true
|
||||
- uses: andresz1/size-limit-action@v1
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
build_script: build:umd
|
||||
skip_step: install
|
||||
directory: src/packages/excalidraw
|
@@ -165,3 +165,35 @@ function App() {
|
||||
| Prop | Type | Required | Default | Description |
|
||||
| --- | --- | :-: | :-: | --- |
|
||||
| `children ` | `React.ReactNode` | Yes | - | The content of the `Menu Group` |
|
||||
|
||||
### MainMenu.Sub
|
||||
|
||||
The MainMenu component now supports submenus. To render a submenu, you can use `MainMenu.Sub`, `MainMenu.Sub.Trigger`, `MainMenu.Sub.Content` and `MainMenu.Sub.Item`. Note that `MainMenu.Sub.Trigger` and `MainMenu.Sub.Content` must be direct children of `MainMenu.Sub`.
|
||||
|
||||
```jsx live
|
||||
function App() {
|
||||
return (
|
||||
<div style={{ height: "500px" }}>
|
||||
<Excalidraw>
|
||||
<MainMenu>
|
||||
<MainMenu.Sub>
|
||||
<MainMenu.Sub.Trigger>Submenu</MainMenu.Sub.Trigger>
|
||||
<MainMenu.Sub.Content>
|
||||
<MainMenu.Sub.Item
|
||||
onSelect={() => window.alert("Submenu item 1")}
|
||||
>
|
||||
Submenu item 1
|
||||
</MainMenu.Sub.Item>
|
||||
<MainMenu.Sub.Item
|
||||
onSelect={() => window.alert("Submenu item 2")}
|
||||
>
|
||||
Submenu item 2
|
||||
</MainMenu.Sub.Item>
|
||||
</MainMenu.Sub.Content>
|
||||
</MainMenu.Sub>
|
||||
</MainMenu>
|
||||
</Excalidraw>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
@@ -65,7 +65,7 @@ export const actionChangeExportScale = register({
|
||||
);
|
||||
|
||||
const scaleButtonTitle = `${t(
|
||||
"buttons.scale",
|
||||
"imageExportDialog.label.scale",
|
||||
)} ${s}x (${width}x${height})`;
|
||||
|
||||
return (
|
||||
@@ -102,7 +102,7 @@ export const actionChangeExportBackground = register({
|
||||
checked={appState.exportBackground}
|
||||
onChange={(checked) => updateData(checked)}
|
||||
>
|
||||
{t("labels.withBackground")}
|
||||
{t("imageExportDialog.label.withBackground")}
|
||||
</CheckboxItem>
|
||||
),
|
||||
});
|
||||
@@ -121,8 +121,8 @@ export const actionChangeExportEmbedScene = register({
|
||||
checked={appState.exportEmbedScene}
|
||||
onChange={(checked) => updateData(checked)}
|
||||
>
|
||||
{t("labels.exportEmbedScene")}
|
||||
<Tooltip label={t("labels.exportEmbedScene_details")} long={true}>
|
||||
{t("imageExportDialog.label.embedScene")}
|
||||
<Tooltip label={t("imageExportDialog.tooltip.embedScene")} long={true}>
|
||||
<div className="excalidraw-tooltip-icon">{questionCircle}</div>
|
||||
</Tooltip>
|
||||
</CheckboxItem>
|
||||
@@ -277,7 +277,7 @@ export const actionExportWithDarkMode = register({
|
||||
onChange={(theme: Theme) => {
|
||||
updateData(theme === THEME.DARK);
|
||||
}}
|
||||
title={t("labels.toggleExportColorScheme")}
|
||||
title={t("imageExportDialog.label.darkMode")}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
|
@@ -8,7 +8,7 @@ import {
|
||||
} from "./colorPickerUtils";
|
||||
import HotkeyLabel from "./HotkeyLabel";
|
||||
import { ColorPaletteCustom } from "../../colors";
|
||||
import { t } from "../../i18n";
|
||||
import { TranslationKeys, t } from "../../i18n";
|
||||
|
||||
interface PickerColorListProps {
|
||||
palette: ColorPaletteCustom;
|
||||
@@ -48,7 +48,11 @@ const PickerColorList = ({
|
||||
(Array.isArray(value) ? value[activeShade] : value) || "transparent";
|
||||
|
||||
const keybinding = colorPickerHotkeyBindings[index];
|
||||
const label = t(`colors.${key.replace(/\d+/, "")}`, null, "");
|
||||
const label = t(
|
||||
`colors.${key.replace(/\d+/, "")}` as unknown as TranslationKeys,
|
||||
null,
|
||||
"",
|
||||
);
|
||||
|
||||
return (
|
||||
<button
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import clsx from "clsx";
|
||||
import { Popover } from "./Popover";
|
||||
import { t } from "../i18n";
|
||||
import { t, TranslationKeys } from "../i18n";
|
||||
|
||||
import "./ContextMenu.scss";
|
||||
import {
|
||||
@@ -83,10 +83,14 @@ export const ContextMenu = React.memo(
|
||||
if (item.contextItemLabel) {
|
||||
if (typeof item.contextItemLabel === "function") {
|
||||
label = t(
|
||||
item.contextItemLabel(elements, appState, actionManager.app),
|
||||
item.contextItemLabel(
|
||||
elements,
|
||||
appState,
|
||||
actionManager.app,
|
||||
) as unknown as TranslationKeys,
|
||||
);
|
||||
} else {
|
||||
label = t(item.contextItemLabel);
|
||||
label = t(item.contextItemLabel as unknown as TranslationKeys);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -3,7 +3,7 @@ import { t } from "../i18n";
|
||||
import { useExcalidrawContainer } from "./App";
|
||||
|
||||
export const Section: React.FC<{
|
||||
heading: string;
|
||||
heading: "canvasActions" | "selectedShapeActions" | "shapes";
|
||||
children?: React.ReactNode | ((heading: React.ReactNode) => React.ReactNode);
|
||||
className?: string;
|
||||
}> = ({ heading, children, ...props }) => {
|
||||
|
@@ -3,6 +3,7 @@ import { render } from "@testing-library/react";
|
||||
import fallbackLangData from "../locales/en.json";
|
||||
|
||||
import Trans from "./Trans";
|
||||
import { TranslationKeys } from "../i18n";
|
||||
|
||||
describe("Test <Trans/>", () => {
|
||||
it("should translate the the strings correctly", () => {
|
||||
@@ -18,24 +19,27 @@ describe("Test <Trans/>", () => {
|
||||
const { getByTestId } = render(
|
||||
<>
|
||||
<div data-testid="test1">
|
||||
<Trans i18nKey="transTest.key1" audience="world" />
|
||||
<Trans
|
||||
i18nKey={"transTest.key1" as unknown as TranslationKeys}
|
||||
audience="world"
|
||||
/>
|
||||
</div>
|
||||
<div data-testid="test2">
|
||||
<Trans
|
||||
i18nKey="transTest.key2"
|
||||
i18nKey={"transTest.key2" as unknown as TranslationKeys}
|
||||
link={(el) => <a href="https://example.com">{el}</a>}
|
||||
/>
|
||||
</div>
|
||||
<div data-testid="test3">
|
||||
<Trans
|
||||
i18nKey="transTest.key3"
|
||||
i18nKey={"transTest.key3" as unknown as TranslationKeys}
|
||||
link={(el) => <a href="https://example.com">{el}</a>}
|
||||
location="the button"
|
||||
/>
|
||||
</div>
|
||||
<div data-testid="test4">
|
||||
<Trans
|
||||
i18nKey="transTest.key4"
|
||||
i18nKey={"transTest.key4" as unknown as TranslationKeys}
|
||||
link={(el) => <a href="https://example.com">{el}</a>}
|
||||
location="the button"
|
||||
bold={(el) => <strong>{el}</strong>}
|
||||
@@ -43,7 +47,7 @@ describe("Test <Trans/>", () => {
|
||||
</div>
|
||||
<div data-testid="test5">
|
||||
<Trans
|
||||
i18nKey="transTest.key5"
|
||||
i18nKey={"transTest.key5" as unknown as TranslationKeys}
|
||||
connect-link={(el) => <a href="https://example.com">{el}</a>}
|
||||
/>
|
||||
</div>
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
|
||||
import { useI18n } from "../i18n";
|
||||
import { TranslationKeys, useI18n } from "../i18n";
|
||||
|
||||
// Used for splitting i18nKey into tokens in Trans component
|
||||
// Example:
|
||||
@@ -153,7 +153,7 @@ const Trans = ({
|
||||
children,
|
||||
...props
|
||||
}: {
|
||||
i18nKey: string;
|
||||
i18nKey: TranslationKeys;
|
||||
[key: string]: React.ReactNode | ((el: React.ReactNode) => React.ReactNode);
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
|
@@ -27,7 +27,6 @@ import { LinearElementEditor } from "./linearElementEditor";
|
||||
import { arrayToMap, tupleToCoors } from "../utils";
|
||||
import { KEYS } from "../keys";
|
||||
import { getBoundTextElement, handleBindTextResize } from "./textElement";
|
||||
import { getContainingFrame, isPointInFrame } from "../frame";
|
||||
|
||||
export type SuggestedBinding =
|
||||
| NonDeleted<ExcalidrawBindableElement>
|
||||
@@ -275,18 +274,6 @@ export const getHoveredElementForBinding = (
|
||||
isBindableElement(element, false) &&
|
||||
bindingBorderTest(element, pointerCoords),
|
||||
);
|
||||
|
||||
if (hoveredElement) {
|
||||
const frame = getContainingFrame(hoveredElement);
|
||||
|
||||
if (frame) {
|
||||
if (isPointInFrame(pointerCoords, frame)) {
|
||||
return hoveredElement as NonDeleted<ExcalidrawBindableElement>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return hoveredElement as NonDeleted<ExcalidrawBindableElement> | null;
|
||||
};
|
||||
|
||||
@@ -512,22 +499,10 @@ const getElligibleElementsForBindingElement = (
|
||||
return [
|
||||
getElligibleElementForBindingElement(linearElement, "start"),
|
||||
getElligibleElementForBindingElement(linearElement, "end"),
|
||||
].filter((element): element is NonDeleted<ExcalidrawBindableElement> => {
|
||||
if (element != null) {
|
||||
const frame = getContainingFrame(element);
|
||||
return frame
|
||||
? isPointInFrame(
|
||||
getLinearElementEdgeCoors(linearElement, "start"),
|
||||
frame,
|
||||
) ||
|
||||
isPointInFrame(
|
||||
getLinearElementEdgeCoors(linearElement, "end"),
|
||||
frame,
|
||||
)
|
||||
: true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
].filter(
|
||||
(element): element is NonDeleted<ExcalidrawBindableElement> =>
|
||||
element != null,
|
||||
);
|
||||
};
|
||||
|
||||
const getElligibleElementForBindingElement = (
|
||||
|
10
src/frame.ts
10
src/frame.ts
@@ -1,7 +1,6 @@
|
||||
import {
|
||||
getCommonBounds,
|
||||
getElementAbsoluteCoords,
|
||||
getElementBounds,
|
||||
isTextElement,
|
||||
} from "./element";
|
||||
import {
|
||||
@@ -300,15 +299,6 @@ export const groupsAreCompletelyOutOfFrame = (
|
||||
);
|
||||
};
|
||||
|
||||
export const isPointInFrame = (
|
||||
{ x, y }: { x: number; y: number },
|
||||
frame: ExcalidrawFrameElement,
|
||||
) => {
|
||||
const [x1, y1, x2, y2] = getElementBounds(frame);
|
||||
|
||||
return x >= x1 && x <= x2 && y >= y1 && y <= y2;
|
||||
};
|
||||
|
||||
// --------------------------- Frame Utils ------------------------------------
|
||||
|
||||
/**
|
||||
|
@@ -3,6 +3,7 @@ import percentages from "./locales/percentages.json";
|
||||
import { ENV } from "./constants";
|
||||
import { jotaiScope, jotaiStore } from "./jotai";
|
||||
import { atom, useAtomValue } from "jotai";
|
||||
import { NestedKeyOf } from "./utility-types";
|
||||
|
||||
const COMPLETION_THRESHOLD = 85;
|
||||
|
||||
@@ -12,6 +13,8 @@ export interface Language {
|
||||
rtl?: boolean;
|
||||
}
|
||||
|
||||
export type TranslationKeys = NestedKeyOf<typeof fallbackLangData>;
|
||||
|
||||
export const defaultLang = { code: "en", label: "English" };
|
||||
|
||||
export const languages: Language[] = [
|
||||
@@ -123,7 +126,7 @@ const findPartsForData = (data: any, parts: string[]) => {
|
||||
};
|
||||
|
||||
export const t = (
|
||||
path: string,
|
||||
path: NestedKeyOf<typeof fallbackLangData>,
|
||||
replacement?: { [key: string]: string | number } | null,
|
||||
fallback?: string,
|
||||
) => {
|
||||
|
16
src/packages/excalidraw/.size-limit.json
Normal file
16
src/packages/excalidraw/.size-limit.json
Normal file
@@ -0,0 +1,16 @@
|
||||
[
|
||||
{
|
||||
"path": "dist/excalidraw.production.min.js",
|
||||
"limit": "285 kB"
|
||||
},
|
||||
{
|
||||
"path": "dist/excalidraw-assets/locales",
|
||||
"name": "dist/excalidraw-assets/locales",
|
||||
"limit": "270 kB"
|
||||
},
|
||||
{
|
||||
"path": "dist/excalidraw-assets/vendor-*.js",
|
||||
"name": "dist/excalidraw-assets/vendor*.js",
|
||||
"limit": "30 kB"
|
||||
}
|
||||
]
|
@@ -52,6 +52,7 @@
|
||||
"@babel/preset-env": "7.18.6",
|
||||
"@babel/preset-react": "7.18.6",
|
||||
"@babel/preset-typescript": "7.18.6",
|
||||
"@size-limit/preset-big-lib": "8.2.6",
|
||||
"autoprefixer": "10.4.7",
|
||||
"babel-loader": "8.2.5",
|
||||
"babel-plugin-transform-class-properties": "6.24.1",
|
||||
@@ -61,6 +62,8 @@
|
||||
"mini-css-extract-plugin": "2.6.1",
|
||||
"postcss-loader": "7.0.1",
|
||||
"sass-loader": "13.0.2",
|
||||
"size-limit": "8.2.4",
|
||||
"style-loader": "3.3.3",
|
||||
"terser-webpack-plugin": "5.3.3",
|
||||
"ts-loader": "9.3.1",
|
||||
"typescript": "4.9.4",
|
||||
@@ -79,6 +82,7 @@
|
||||
"pack": "yarn build:umd && yarn pack",
|
||||
"start": "webpack serve --config webpack.dev-server.config.js",
|
||||
"install:deps": "yarn install --frozen-lockfile && yarn --cwd ../../../",
|
||||
"build:example": "EXAMPLE=true webpack --config webpack.dev-server.config.js && yarn gen:types"
|
||||
"build:example": "EXAMPLE=true webpack --config webpack.dev-server.config.js && yarn gen:types",
|
||||
"size": "yarn build:umd && size-limit"
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -50,3 +50,7 @@ export type ExtractSetType<T extends Set<any>> = T extends Set<infer U>
|
||||
|
||||
export type SameType<T, U> = T extends U ? (U extends T ? true : false) : false;
|
||||
export type Assert<T extends true> = T;
|
||||
|
||||
export type NestedKeyOf<T, K = keyof T> = K extends keyof T & (string | number)
|
||||
? `${K}` | (T[K] extends object ? `${K}.${NestedKeyOf<T[K]>}` : never)
|
||||
: never;
|
||||
|
12
yarn.lock
12
yarn.lock
@@ -10267,7 +10267,7 @@ terser-webpack-plugin@^5.1.3, terser-webpack-plugin@^5.2.5:
|
||||
serialize-javascript "^6.0.1"
|
||||
terser "^5.16.5"
|
||||
|
||||
terser@^5.0.0, terser@^5.10.0, terser@^5.16.5:
|
||||
terser@^5.0.0, terser@^5.10.0:
|
||||
version "5.16.9"
|
||||
resolved "https://registry.yarnpkg.com/terser/-/terser-5.16.9.tgz#7a28cb178e330c484369886f2afd623d9847495f"
|
||||
integrity sha512-HPa/FdTB9XGI2H1/keLFZHxl6WNvAI4YalHGtDQTlMnJcoqSab1UwL4l1hGEhs6/GmLHBZIg/YgB++jcbzoOEg==
|
||||
@@ -10277,6 +10277,16 @@ terser@^5.0.0, terser@^5.10.0, terser@^5.16.5:
|
||||
commander "^2.20.0"
|
||||
source-map-support "~0.5.20"
|
||||
|
||||
terser@^5.16.5:
|
||||
version "5.17.1"
|
||||
resolved "https://registry.yarnpkg.com/terser/-/terser-5.17.1.tgz#948f10830454761e2eeedc6debe45c532c83fd69"
|
||||
integrity sha512-hVl35zClmpisy6oaoKALOpS0rDYLxRFLHhRuDlEGTKey9qHjS1w9GMORjuwIMt70Wan4lwsLYyWDVnWgF+KUEw==
|
||||
dependencies:
|
||||
"@jridgewell/source-map" "^0.3.2"
|
||||
acorn "^8.5.0"
|
||||
commander "^2.20.0"
|
||||
source-map-support "~0.5.20"
|
||||
|
||||
test-exclude@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e"
|
||||
|
Reference in New Issue
Block a user