Compare commits

...
Author SHA1 Message Date
dwelle b8da178ebd feat: update eye-dropper icon 2023-07-07 12:22:52 +02:00
zsviczianandGitHub 3ddcc48e4c fix: UI disappears when pressing the eyedropper shortcut on mobile (#6725) 2023-06-29 12:39:44 +02:00
29a5e982c3 feat: support scrollToContent opts.fitToViewport (#6581)
Co-authored-by: dwelle <luzar.david@gmail.com>
Co-authored-by: Arnošt Pleskot <arnostpleskot@gmail.com>
2023-06-29 12:36:38 +02:00
b33fa6d6f6 fix: stronger enforcement of normalizeLink (#6728)
Co-authored-by: dwelle <luzar.david@gmail.com>
2023-06-29 12:14:42 +02:00
b7350f9707 fix: elements in non-existing frame getting removed (#6708)
Co-authored-by: dwelle <luzar.david@gmail.com>
2023-06-22 22:10:08 +00:00
David LuzarandGitHub 8dfa2a98bb fix: scrollbars renders but disable (#6706) 2023-06-22 14:35:01 +02:00
Wu KaiandGitHub fb01ce2a00 fix: typo in chart.ts (#6696)
fix: typo
2023-06-21 11:43:37 +05:30
Excalidraw BotandGitHub 3d57112480 chore: Update translations from Crowdin (#6677) 2023-06-19 17:10:09 +02:00
7558a4e2be feat: overwrite confirmation dialogs (#6658)
Co-authored-by: dwelle <luzar.david@gmail.com>
2023-06-19 17:08:12 +02:00
Aakansha DoshiandGitHub 6d56634289 fix: do not bind text to container using text tool when it has text already (#6694)
* fix: do not bind text to container using text tool when it has text already

* Update src/element/textWysiwyg.test.tsx
2023-06-19 17:28:45 +05:30
Milos VetesnikandGitHub 0aa1e66486 feat: simple analitycs (#6683)
* Simple analytics for iframe and webpage

* added logic for tracking specific categories of events to reduce it

* enviroment vars clean up

* fix: lint for index.html
2023-06-19 11:18:28 +02:00
Aakansha DoshiandGitHub 7f7128ec09 fix: don't allow binding text to images (#6693) 2023-06-19 13:47:28 +05:30
Aakansha DoshiandGitHub 6de6a96abf docs: add info about roadmap (#6687) 2023-06-16 20:55:33 +05:30
28ab6531c9 fix: updated link for documentation page under help section (#6654)
* fix: updated link for documentation page under help section

* Update docs link

---------

Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
2023-06-15 14:58:11 +05:30
110 changed files with 1905 additions and 479 deletions
+1 -5
View File
@@ -20,14 +20,10 @@ REACT_APP_DEV_ENABLE_SW=
# whether to disable live reload / HMR. Usuaully what you want to do when # whether to disable live reload / HMR. Usuaully what you want to do when
# debugging Service Workers. # debugging Service Workers.
REACT_APP_DEV_DISABLE_LIVE_RELOAD= REACT_APP_DEV_DISABLE_LIVE_RELOAD=
REACT_APP_DISABLE_TRACKING=true
FAST_REFRESH=false FAST_REFRESH=false
# MATOMO
REACT_APP_MATOMO_URL=
REACT_APP_CDN_MATOMO_TRACKER_URL=
REACT_APP_MATOMO_SITE_ID=
#Debug flags #Debug flags
# To enable bounding box for text containers # To enable bounding box for text containers
+1 -10
View File
@@ -11,14 +11,5 @@ REACT_APP_WS_SERVER_URL=
REACT_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyAd15pYlMci_xIp9ko6wkEsDzAAA0Dn0RU","authDomain":"excalidraw-room-persistence.firebaseapp.com","databaseURL":"https://excalidraw-room-persistence.firebaseio.com","projectId":"excalidraw-room-persistence","storageBucket":"excalidraw-room-persistence.appspot.com","messagingSenderId":"654800341332","appId":"1:654800341332:web:4a692de832b55bd57ce0c1"}' REACT_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyAd15pYlMci_xIp9ko6wkEsDzAAA0Dn0RU","authDomain":"excalidraw-room-persistence.firebaseapp.com","databaseURL":"https://excalidraw-room-persistence.firebaseio.com","projectId":"excalidraw-room-persistence","storageBucket":"excalidraw-room-persistence.appspot.com","messagingSenderId":"654800341332","appId":"1:654800341332:web:4a692de832b55bd57ce0c1"}'
# production-only vars
# GOOGLE ANALYTICS
REACT_APP_GOOGLE_ANALYTICS_ID=UA-387204-13
# MATOMO
REACT_APP_MATOMO_URL=https://excalidraw.matomo.cloud/
REACT_APP_CDN_MATOMO_TRACKER_URL=//cdn.matomo.cloud/excalidraw.matomo.cloud/matomo.js
REACT_APP_MATOMO_SITE_ID=1
REACT_APP_PLUS_APP=https://app.excalidraw.com REACT_APP_PLUS_APP=https://app.excalidraw.com
REACT_APP_DISABLE_TRACKING=
@@ -306,30 +306,32 @@ This is the history API. history.clear() will clear the history.
## scrollToContent ## scrollToContent
<pre> ```tsx
(<br /> (
{" "} target?: ExcalidrawElement | ExcalidrawElement[],
target?:{" "} opts?:
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L115"> | {
ExcalidrawElement fitToContent?: boolean;
</a>{" "} animate?: boolean;
&#124;{" "} duration?: number;
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L115"> }
ExcalidrawElement | {
</a> fitToViewport?: boolean;
[], viewportZoomFactor?: number;
<br /> animate?: boolean;
{" "}opts?: &#123; fitToContent?: boolean; animate?: boolean; duration?: number duration?: number;
&#125; }
<br />) => void ) => void
</pre> ```
Scroll the nearest element out of the elements supplied to the center of the viewport. Defaults to the elements on the scene. Scroll the nearest element out of the elements supplied to the center of the viewport. Defaults to the elements on the scene.
| Attribute | type | default | Description | | Attribute | type | default | Description |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| target | <code>ExcalidrawElement &#124; ExcalidrawElement[]</code> | All scene elements | The element(s) to scroll to. | | target | [ExcalidrawElement](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L115) &#124; [ExcalidrawElement[]](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L115) | All scene elements | The element(s) to scroll to. |
| opts.fitToContent | boolean | false | Whether to fit the elements to viewport by automatically changing zoom as needed. | | opts.fitToContent | boolean | false | Whether to fit the elements to viewport by automatically changing zoom as needed. Note that the zoom range is between 10%-100%. |
| opts.fitToViewport | boolean | false | Similar to fitToContent but the zoom range is not limited. If elements are smaller than the viewport, zoom will go above 100%. |
| opts.viewportZoomFactor | number | 0.7 | when fitToViewport=true, how much screen should the content cover, between 0.1 (10%) and 1 (100%) |
| opts.animate | boolean | false | Whether to animate between starting and ending position. Note that for larger scenes the animation may not be smooth due to performance issues. | | opts.animate | boolean | false | Whether to animate between starting and ending position. Note that for larger scenes the animation may not be smooth due to performance issues. |
| opts.duration | number | 500 | Duration of the animation if `opts.animate` is `true`. | | opts.duration | number | 500 | Duration of the animation if `opts.animate` is `true`. |
@@ -2,6 +2,11 @@
Pull requests are welcome. For major changes, please [open an issue](https://github.com/excalidraw/excalidraw/issues/new) first to discuss what you would like to change. Pull requests are welcome. For major changes, please [open an issue](https://github.com/excalidraw/excalidraw/issues/new) first to discuss what you would like to change.
We have a [roadmap](https://github.com/orgs/excalidraw/projects/3) which we strongly recommend to go through and check if something interests you.
For new contributors we would recommend to start with *Easy* tasks.
In case you want to pick up something from the roadmap, comment on that issue and one of the project maintainers will assign it to you, post which you can discuss in the issue and start working on it.
## Setup ## Setup
### Option 1 - Manual ### Option 1 - Manual
+1
View File
@@ -19,6 +19,7 @@
] ]
}, },
"dependencies": { "dependencies": {
"@braintree/sanitize-url": "6.0.2",
"@excalidraw/random-username": "1.0.0", "@excalidraw/random-username": "1.0.0",
"@radix-ui/react-popover": "1.0.3", "@radix-ui/react-popover": "1.0.3",
"@radix-ui/react-tabs": "1.0.2", "@radix-ui/react-tabs": "1.0.2",
+33 -38
View File
@@ -148,33 +148,6 @@
// setting this so that libraries installation reuses this window tab. // setting this so that libraries installation reuses this window tab.
window.name = "_excalidraw"; window.name = "_excalidraw";
</script> </script>
<% if (process.env.REACT_APP_DISABLE_TRACKING !== 'true') { %>
<!-- Fathom - privacy-friendly analytics -->
<script
src="https://cdn.usefathom.com/script.js"
data-site="VMSBUEYA"
defer
></script>
<!-- / Fathom -->
<!-- LEGACY GOOGLE ANALYTICS -->
<% if (process.env.REACT_APP_GOOGLE_ANALYTICS_ID) { %>
<script
async
src="https://www.googletagmanager.com/gtag/js?id=%REACT_APP_GOOGLE_ANALYTICS_ID%"
></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag("js", new Date());
gtag("config", "%REACT_APP_GOOGLE_ANALYTICS_ID%");
</script>
<% } %>
<!-- end LEGACY GOOGLE ANALYTICS -->
<% } %>
<!-- FIXME: remove this when we update CRA (fix SW caching) --> <!-- FIXME: remove this when we update CRA (fix SW caching) -->
<style> <style>
@@ -227,17 +200,39 @@
<h1 class="visually-hidden">Excalidraw</h1> <h1 class="visually-hidden">Excalidraw</h1>
</header> </header>
<div id="root"></div> <div id="root"></div>
<% if (process.env.REACT_APP_DISABLE_TRACKING !== 'true') { %>
<!-- 100% privacy friendly analytics --> <!-- 100% privacy friendly analytics -->
<script <script>
async // need to load this script dynamically bcs. of iframe embed tracking
defer var scriptEle = document.createElement("script");
src="https://scripts.simpleanalyticscdn.com/latest.js" scriptEle.setAttribute(
></script> "src",
<noscript "https://scripts.simpleanalyticscdn.com/latest.js",
><img );
src="https://queue.simpleanalyticscdn.com/noscript.gif" scriptEle.setAttribute("type", "text/javascript");
alt="" scriptEle.setAttribute("defer", true);
referrerpolicy="no-referrer-when-downgrade" scriptEle.setAttribute("async", true);
/></noscript> // if iframe
if (window.self !== window.top) {
scriptEle.setAttribute("data-auto-collect", true);
}
document.body.appendChild(scriptEle);
// if iframe
if (window.self !== window.top) {
scriptEle.addEventListener("load", () => {
if (window.sa_pageview) {
window.window.sa_event(action, {
category: "iframe",
label: "embed",
value: window.location.pathname,
});
}
});
}
</script>
<!-- end LEGACY GOOGLE ANALYTICS -->
<% } %>
</body> </body>
</html> </html>
+101 -35
View File
@@ -20,7 +20,6 @@ import {
isHandToolActive, isHandToolActive,
} from "../appState"; } from "../appState";
import { DEFAULT_CANVAS_BACKGROUND_PICKS } from "../colors"; import { DEFAULT_CANVAS_BACKGROUND_PICKS } from "../colors";
import { excludeElementsInFramesFromSelection } from "../scene/selection";
import { Bounds } from "../element/bounds"; import { Bounds } from "../element/bounds";
export const actionChangeViewBackgroundColor = register({ export const actionChangeViewBackgroundColor = register({
@@ -226,52 +225,96 @@ const zoomValueToFitBoundsOnViewport = (
return clampedZoomValueToFitElements as NormalizedZoomValue; return clampedZoomValueToFitElements as NormalizedZoomValue;
}; };
export const zoomToFitElements = ( export const zoomToFit = ({
elements: readonly ExcalidrawElement[], targetElements,
appState: Readonly<AppState>, appState,
zoomToSelection: boolean, fitToViewport = false,
) => { viewportZoomFactor = 0.7,
const nonDeletedElements = getNonDeletedElements(elements); }: {
const selectedElements = getSelectedElements(nonDeletedElements, appState); targetElements: readonly ExcalidrawElement[];
appState: Readonly<AppState>;
const commonBounds = /** whether to fit content to viewport (beyond >100%) */
zoomToSelection && selectedElements.length > 0 fitToViewport: boolean;
? getCommonBounds(excludeElementsInFramesFromSelection(selectedElements)) /** zoom content to cover X of the viewport, when fitToViewport=true */
: getCommonBounds( viewportZoomFactor?: number;
excludeElementsInFramesFromSelection(nonDeletedElements), }) => {
); const commonBounds = getCommonBounds(getNonDeletedElements(targetElements));
const newZoom = {
value: zoomValueToFitBoundsOnViewport(commonBounds, {
width: appState.width,
height: appState.height,
}),
};
const [x1, y1, x2, y2] = commonBounds; const [x1, y1, x2, y2] = commonBounds;
const centerX = (x1 + x2) / 2; const centerX = (x1 + x2) / 2;
const centerY = (y1 + y2) / 2; const centerY = (y1 + y2) / 2;
let newZoomValue;
let scrollX;
let scrollY;
if (fitToViewport) {
const commonBoundsWidth = x2 - x1;
const commonBoundsHeight = y2 - y1;
newZoomValue =
Math.min(
appState.width / commonBoundsWidth,
appState.height / commonBoundsHeight,
) * Math.min(1, Math.max(viewportZoomFactor, 0.1));
// Apply clamping to newZoomValue to be between 10% and 3000%
newZoomValue = Math.min(
Math.max(newZoomValue, 0.1),
30.0,
) as NormalizedZoomValue;
scrollX = (appState.width / 2) * (1 / newZoomValue) - centerX;
scrollY = (appState.height / 2) * (1 / newZoomValue) - centerY;
} else {
newZoomValue = zoomValueToFitBoundsOnViewport(commonBounds, {
width: appState.width,
height: appState.height,
});
const centerScroll = centerScrollOn({
scenePoint: { x: centerX, y: centerY },
viewportDimensions: {
width: appState.width,
height: appState.height,
},
zoom: { value: newZoomValue },
});
scrollX = centerScroll.scrollX;
scrollY = centerScroll.scrollY;
}
return { return {
appState: { appState: {
...appState, ...appState,
...centerScrollOn({ scrollX,
scenePoint: { x: centerX, y: centerY }, scrollY,
viewportDimensions: { zoom: { value: newZoomValue },
width: appState.width,
height: appState.height,
},
zoom: newZoom,
}),
zoom: newZoom,
}, },
commitToHistory: false, commitToHistory: false,
}; };
}; };
export const actionZoomToSelected = register({ // Note, this action differs from actionZoomToFitSelection in that it doesn't
name: "zoomToSelection", // zoom beyond 100%. In other words, if the content is smaller than viewport
// size, it won't be zoomed in.
export const actionZoomToFitSelectionInViewport = register({
name: "zoomToFitSelectionInViewport",
trackEvent: { category: "canvas" }, trackEvent: { category: "canvas" },
perform: (elements, appState) => zoomToFitElements(elements, appState, true), perform: (elements, appState) => {
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
);
return zoomToFit({
targetElements: selectedElements.length ? selectedElements : elements,
appState,
fitToViewport: false,
});
},
// NOTE shift-2 should have been assigned actionZoomToFitSelection.
// TBD on how proceed
keyTest: (event) => keyTest: (event) =>
event.code === CODES.TWO && event.code === CODES.TWO &&
event.shiftKey && event.shiftKey &&
@@ -279,11 +322,34 @@ export const actionZoomToSelected = register({
!event[KEYS.CTRL_OR_CMD], !event[KEYS.CTRL_OR_CMD],
}); });
export const actionZoomToFitSelection = register({
name: "zoomToFitSelection",
trackEvent: { category: "canvas" },
perform: (elements, appState) => {
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
);
return zoomToFit({
targetElements: selectedElements.length ? selectedElements : elements,
appState,
fitToViewport: true,
});
},
// NOTE this action should use shift-2 per figma, alas
keyTest: (event) =>
event.code === CODES.THREE &&
event.shiftKey &&
!event.altKey &&
!event[KEYS.CTRL_OR_CMD],
});
export const actionZoomToFit = register({ export const actionZoomToFit = register({
name: "zoomToFit", name: "zoomToFit",
viewMode: true, viewMode: true,
trackEvent: { category: "canvas" }, trackEvent: { category: "canvas" },
perform: (elements, appState) => zoomToFitElements(elements, appState, false), perform: (elements, appState) =>
zoomToFit({ targetElements: elements, appState, fitToViewport: false }),
keyTest: (event) => keyTest: (event) =>
event.code === CODES.ONE && event.code === CODES.ONE &&
event.shiftKey && event.shiftKey &&
+2 -1
View File
@@ -82,7 +82,8 @@ export type ActionName =
| "zoomOut" | "zoomOut"
| "resetZoom" | "resetZoom"
| "zoomToFit" | "zoomToFit"
| "zoomToSelection" | "zoomToFitSelection"
| "zoomToFitSelectionInViewport"
| "changeFontFamily" | "changeFontFamily"
| "changeTextAlign" | "changeTextAlign"
| "changeVerticalAlign" | "changeVerticalAlign"
+5 -14
View File
@@ -5,6 +5,9 @@ export const trackEvent = (
value?: number, value?: number,
) => { ) => {
try { try {
// place here categories that you want to track as events
// KEEP IN MIND THE PRICING
const ALLOWED_CATEGORIES_TO_TRACK = [] as string[];
// Uncomment the next line to track locally // Uncomment the next line to track locally
// console.log("Track Event", { category, action, label, value }); // console.log("Track Event", { category, action, label, value });
@@ -12,12 +15,8 @@ export const trackEvent = (
return; return;
} }
if (process.env.REACT_APP_GOOGLE_ANALYTICS_ID && window.gtag) { if (!ALLOWED_CATEGORIES_TO_TRACK.includes(category)) {
window.gtag("event", action, { return;
event_category: category,
event_label: label,
value,
});
} }
if (window.sa_event) { if (window.sa_event) {
@@ -27,14 +26,6 @@ export const trackEvent = (
value, value,
}); });
} }
if (window.fathom) {
window.fathom.trackEvent(action, {
category,
label,
value,
});
}
} catch (error) { } catch (error) {
console.error("error during analytics", error); console.error("error during analytics", error);
} }
+3 -3
View File
@@ -180,7 +180,7 @@ const commonProps = {
locked: false, locked: false,
} as const; } as const;
const getChartDimentions = (spreadsheet: Spreadsheet) => { const getChartDimensions = (spreadsheet: Spreadsheet) => {
const chartWidth = const chartWidth =
(BAR_WIDTH + BAR_GAP) * spreadsheet.values.length + BAR_GAP; (BAR_WIDTH + BAR_GAP) * spreadsheet.values.length + BAR_GAP;
const chartHeight = BAR_HEIGHT + BAR_GAP * 2; const chartHeight = BAR_HEIGHT + BAR_GAP * 2;
@@ -250,7 +250,7 @@ const chartLines = (
groupId: string, groupId: string,
backgroundColor: string, backgroundColor: string,
): ChartElements => { ): ChartElements => {
const { chartWidth, chartHeight } = getChartDimentions(spreadsheet); const { chartWidth, chartHeight } = getChartDimensions(spreadsheet);
const xLine = newLinearElement({ const xLine = newLinearElement({
backgroundColor, backgroundColor,
groupIds: [groupId], groupIds: [groupId],
@@ -313,7 +313,7 @@ const chartBaseElements = (
backgroundColor: string, backgroundColor: string,
debug?: boolean, debug?: boolean,
): ChartElements => { ): ChartElements => {
const { chartWidth, chartHeight } = getChartDimentions(spreadsheet); const { chartWidth, chartHeight } = getChartDimensions(spreadsheet);
const title = spreadsheet.title const title = spreadsheet.title
? newTextElement({ ? newTextElement({
+83 -29
View File
@@ -245,6 +245,7 @@ import {
isTransparent, isTransparent,
easeToValuesRAF, easeToValuesRAF,
muteFSAbortError, muteFSAbortError,
easeOut,
} from "../utils"; } from "../utils";
import { import {
ContextMenu, ContextMenu,
@@ -291,13 +292,12 @@ import {
} from "../element/textElement"; } from "../element/textElement";
import { isHittingElementNotConsideringBoundingBox } from "../element/collision"; import { isHittingElementNotConsideringBoundingBox } from "../element/collision";
import { import {
normalizeLink,
showHyperlinkTooltip, showHyperlinkTooltip,
hideHyperlinkToolip, hideHyperlinkToolip,
Hyperlink, Hyperlink,
isPointHittingLinkIcon, isPointHittingLinkIcon,
isLocalLink,
} from "../element/Hyperlink"; } from "../element/Hyperlink";
import { isLocalLink, normalizeLink } from "../data/url";
import { shouldShowBoundingBox } from "../element/transformHandles"; import { shouldShowBoundingBox } from "../element/transformHandles";
import { actionUnlockAllElements } from "../actions/actionElementLock"; import { actionUnlockAllElements } from "../actions/actionElementLock";
import { Fonts } from "../scene/Fonts"; import { Fonts } from "../scene/Fonts";
@@ -321,10 +321,7 @@ import {
actionRemoveAllElementsFromFrame, actionRemoveAllElementsFromFrame,
actionSelectAllElementsInFrame, actionSelectAllElementsInFrame,
} from "../actions/actionFrame"; } from "../actions/actionFrame";
import { import { actionToggleHandTool, zoomToFit } from "../actions/actionCanvas";
actionToggleHandTool,
zoomToFitElements,
} from "../actions/actionCanvas";
import { jotaiStore } from "../jotai"; import { jotaiStore } from "../jotai";
import { activeConfirmDialogAtom } from "./ActiveConfirmDialog"; import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
import { actionWrapTextInContainer } from "../actions/actionBoundText"; import { actionWrapTextInContainer } from "../actions/actionBoundText";
@@ -1720,7 +1717,7 @@ class App extends React.Component<AppProps, AppState> {
theme: this.state.theme, theme: this.state.theme,
imageCache: this.imageCache, imageCache: this.imageCache,
isExporting: false, isExporting: false,
renderScrollbars: !this.device.isMobile, renderScrollbars: false,
}, },
callback: ({ atLeastOneVisibleElement, scrollBars }) => { callback: ({ atLeastOneVisibleElement, scrollBars }) => {
if (scrollBars) { if (scrollBars) {
@@ -2240,27 +2237,51 @@ class App extends React.Component<AppProps, AppState> {
target: target:
| ExcalidrawElement | ExcalidrawElement
| readonly ExcalidrawElement[] = this.scene.getNonDeletedElements(), | readonly ExcalidrawElement[] = this.scene.getNonDeletedElements(),
opts?: { fitToContent?: boolean; animate?: boolean; duration?: number }, opts?:
| {
fitToContent?: boolean;
fitToViewport?: never;
viewportZoomFactor?: never;
animate?: boolean;
duration?: number;
}
| {
fitToContent?: never;
fitToViewport?: boolean;
/** when fitToViewport=true, how much screen should the content cover,
* between 0.1 (10%) and 1 (100%)
*/
viewportZoomFactor?: number;
animate?: boolean;
duration?: number;
},
) => { ) => {
this.cancelInProgresAnimation?.(); this.cancelInProgresAnimation?.();
// convert provided target into ExcalidrawElement[] if necessary // convert provided target into ExcalidrawElement[] if necessary
const targets = Array.isArray(target) ? target : [target]; const targetElements = Array.isArray(target) ? target : [target];
let zoom = this.state.zoom; let zoom = this.state.zoom;
let scrollX = this.state.scrollX; let scrollX = this.state.scrollX;
let scrollY = this.state.scrollY; let scrollY = this.state.scrollY;
if (opts?.fitToContent) { if (opts?.fitToContent || opts?.fitToViewport) {
// compute an appropriate viewport location (scroll X, Y) and zoom level const { appState } = zoomToFit({
// that fit the target elements on the scene targetElements,
const { appState } = zoomToFitElements(targets, this.state, false); appState: this.state,
fitToViewport: !!opts?.fitToViewport,
viewportZoomFactor: opts?.viewportZoomFactor,
});
zoom = appState.zoom; zoom = appState.zoom;
scrollX = appState.scrollX; scrollX = appState.scrollX;
scrollY = appState.scrollY; scrollY = appState.scrollY;
} else { } else {
// compute only the viewport location, without any zoom adjustment // compute only the viewport location, without any zoom adjustment
const scroll = calculateScrollCenter(targets, this.state, this.canvas); const scroll = calculateScrollCenter(
targetElements,
this.state,
this.canvas,
);
scrollX = scroll.scrollX; scrollX = scroll.scrollX;
scrollY = scroll.scrollY; scrollY = scroll.scrollY;
} }
@@ -2270,19 +2291,42 @@ class App extends React.Component<AppProps, AppState> {
if (opts?.animate) { if (opts?.animate) {
const origScrollX = this.state.scrollX; const origScrollX = this.state.scrollX;
const origScrollY = this.state.scrollY; const origScrollY = this.state.scrollY;
const origZoom = this.state.zoom.value;
// zoom animation could become problematic on scenes with large number const cancel = easeToValuesRAF({
// of elements, setting it to its final value to improve user experience. fromValues: {
// scrollX: origScrollX,
// using zoomCanvas() to zoom on current viewport center scrollY: origScrollY,
this.zoomCanvas(zoom.value); zoom: origZoom,
},
toValues: { scrollX, scrollY, zoom: zoom.value },
interpolateValue: (from, to, progress, key) => {
// for zoom, use different easing
if (key === "zoom") {
return from * Math.pow(to / from, easeOut(progress));
}
// handle using default
return undefined;
},
onStep: ({ scrollX, scrollY, zoom }) => {
this.setState({
scrollX,
scrollY,
zoom: { value: zoom },
});
},
onStart: () => {
this.setState({ shouldCacheIgnoreZoom: true });
},
onEnd: () => {
this.setState({ shouldCacheIgnoreZoom: false });
},
onCancel: () => {
this.setState({ shouldCacheIgnoreZoom: false });
},
duration: opts?.duration ?? 500,
});
const cancel = easeToValuesRAF(
[origScrollX, origScrollY],
[scrollX, scrollY],
(scrollX, scrollY) => this.setState({ scrollX, scrollY }),
{ duration: opts?.duration ?? 500 },
);
this.cancelInProgresAnimation = () => { this.cancelInProgresAnimation = () => {
cancel(); cancel();
this.cancelInProgresAnimation = null; this.cancelInProgresAnimation = null;
@@ -3054,7 +3098,10 @@ class App extends React.Component<AppProps, AppState> {
container, container,
); );
if (container && parentCenterPosition) { if (container && parentCenterPosition) {
shouldBindToContainer = true; const boundTextElementToContainer = getBoundTextElement(container);
if (!boundTextElementToContainer) {
shouldBindToContainer = true;
}
} }
let existingTextElement: NonDeleted<ExcalidrawTextElement> | null = null; let existingTextElement: NonDeleted<ExcalidrawTextElement> | null = null;
@@ -3349,12 +3396,19 @@ class App extends React.Component<AppProps, AppState> {
this.device.isMobile, this.device.isMobile,
); );
if (lastPointerDownHittingLinkIcon && lastPointerUpHittingLinkIcon) { if (lastPointerDownHittingLinkIcon && lastPointerUpHittingLinkIcon) {
const url = this.hitLinkElement.link; let url = this.hitLinkElement.link;
if (url) { if (url) {
url = normalizeLink(url);
let customEvent; let customEvent;
if (this.props.onLinkOpen) { if (this.props.onLinkOpen) {
customEvent = wrapEvent(EVENT.EXCALIDRAW_LINK, event.nativeEvent); customEvent = wrapEvent(EVENT.EXCALIDRAW_LINK, event.nativeEvent);
this.props.onLinkOpen(this.hitLinkElement, customEvent); this.props.onLinkOpen(
{
...this.hitLinkElement,
link: url,
},
customEvent,
);
} }
if (!customEvent?.defaultPrevented) { if (!customEvent?.defaultPrevented) {
const target = isLocalLink(url) ? "_self" : "_blank"; const target = isLocalLink(url) ? "_self" : "_blank";
@@ -3362,7 +3416,7 @@ class App extends React.Component<AppProps, AppState> {
// https://mathiasbynens.github.io/rel-noopener/ // https://mathiasbynens.github.io/rel-noopener/
if (newWindow) { if (newWindow) {
newWindow.opener = null; newWindow.opener = null;
newWindow.location = normalizeLink(url); newWindow.location = url;
} }
} }
} }
+20 -4
View File
@@ -17,16 +17,34 @@ import { useSetAtom } from "jotai";
import { isLibraryMenuOpenAtom } from "./LibraryMenu"; import { isLibraryMenuOpenAtom } from "./LibraryMenu";
import { jotaiScope } from "../jotai"; import { jotaiScope } from "../jotai";
export type DialogSize = number | "small" | "regular" | "wide" | undefined;
export interface DialogProps { export interface DialogProps {
children: React.ReactNode; children: React.ReactNode;
className?: string; className?: string;
size?: "small" | "regular" | "wide"; size?: DialogSize;
onCloseRequest(): void; onCloseRequest(): void;
title: React.ReactNode | false; title: React.ReactNode | false;
autofocus?: boolean; autofocus?: boolean;
closeOnClickOutside?: boolean; closeOnClickOutside?: boolean;
} }
function getDialogSize(size: DialogSize): number {
if (size && typeof size === "number") {
return size;
}
switch (size) {
case "small":
return 550;
case "wide":
return 1024;
case "regular":
default:
return 800;
}
}
export const Dialog = (props: DialogProps) => { export const Dialog = (props: DialogProps) => {
const [islandNode, setIslandNode] = useCallbackRefState<HTMLDivElement>(); const [islandNode, setIslandNode] = useCallbackRefState<HTMLDivElement>();
const [lastActiveElement] = useState(document.activeElement); const [lastActiveElement] = useState(document.activeElement);
@@ -85,9 +103,7 @@ export const Dialog = (props: DialogProps) => {
<Modal <Modal
className={clsx("Dialog", props.className)} className={clsx("Dialog", props.className)}
labelledBy="dialog-title" labelledBy="dialog-title"
maxWidth={ maxWidth={getDialogSize(props.size)}
props.size === "wide" ? 1024 : props.size === "small" ? 550 : 800
}
onCloseRequest={onClose} onCloseRequest={onClose}
closeOnClickOutside={props.closeOnClickOutside} closeOnClickOutside={props.closeOnClickOutside}
> >
+138 -42
View File
@@ -2,20 +2,140 @@
.excalidraw { .excalidraw {
.ExcButton { .ExcButton {
&--color-primary { --text-color: transparent;
color: var(--input-bg-color); --border-color: transparent;
--back-color: transparent;
--accent-color: var(--color-primary); color: var(--text-color);
--accent-color-hover: var(--color-primary-darker); background-color: var(--back-color);
--accent-color-active: var(--color-primary-darkest); border-color: var(--border-color);
&--color-primary {
&.ExcButton--variant-filled {
--text-color: var(--input-bg-color);
--back-color: var(--color-primary);
&:hover {
--back-color: var(--color-primary-darker);
}
&:active {
--back-color: var(--color-primary-darkest);
}
}
&.ExcButton--variant-outlined,
&.ExcButton--variant-icon {
--text-color: var(--color-primary);
--border-color: var(--color-primary);
--back-color: var(--input-bg-color);
&:hover {
--text-color: var(--color-primary-darker);
--border-color: var(--color-primary-darker);
}
&:active {
--text-color: var(--color-primary-darkest);
--border-color: var(--color-primary-darkest);
}
}
} }
&--color-danger { &--color-danger {
color: var(--input-bg-color); &.ExcButton--variant-filled {
--text-color: var(--color-danger-text);
--back-color: var(--color-danger-dark);
--accent-color: var(--color-danger); &:hover {
--accent-color-hover: #d65550; --back-color: var(--color-danger-darker);
--accent-color-active: #d1413c; }
&:active {
--back-color: var(--color-danger-darkest);
}
}
&.ExcButton--variant-outlined,
&.ExcButton--variant-icon {
--text-color: var(--color-danger);
--border-color: var(--color-danger);
--back-color: transparent;
&:hover {
--text-color: var(--color-danger-darkest);
--border-color: var(--color-danger-darkest);
}
&:active {
--text-color: var(--color-danger-darker);
--border-color: var(--color-danger-darker);
}
}
}
&--color-muted {
&.ExcButton--variant-filled {
--text-color: var(--island-bg-color);
--back-color: var(--color-gray-50);
&:hover {
--back-color: var(--color-gray-60);
}
&:active {
--back-color: var(--color-gray-80);
}
}
&.ExcButton--variant-outlined,
&.ExcButton--variant-icon {
--text-color: var(--color-muted-background);
--border-color: var(--color-muted);
--back-color: var(--island-bg-color);
&:hover {
--text-color: var(--color-muted-background-darker);
--border-color: var(--color-muted-darker);
}
&:active {
--text-color: var(--color-muted-background-darker);
--border-color: var(--color-muted-darkest);
}
}
}
&--color-warning {
&.ExcButton--variant-filled {
--text-color: black;
--back-color: var(--color-warning-dark);
&:hover {
--back-color: var(--color-warning-darker);
}
&:active {
--back-color: var(--color-warning-darkest);
}
}
&.ExcButton--variant-outlined,
&.ExcButton--variant-icon {
--text-color: var(--color-warning-dark);
--border-color: var(--color-warning-dark);
--back-color: var(--input-bg-color);
&:hover {
--text-color: var(--color-warning-darker);
--border-color: var(--color-warning-darker);
}
&:active {
--text-color: var(--color-warning-darkest);
--border-color: var(--color-warning-darkest);
}
}
} }
display: flex; display: flex;
@@ -25,6 +145,8 @@
flex-wrap: nowrap; flex-wrap: nowrap;
border-radius: 0.5rem; border-radius: 0.5rem;
border-width: 1px;
border-style: solid;
font-family: "Assistant"; font-family: "Assistant";
@@ -33,9 +155,9 @@
transition: all 150ms ease-out; transition: all 150ms ease-out;
&--size-large { &--size-large {
font-weight: 400; font-weight: 600;
font-size: 0.875rem; font-size: 0.875rem;
height: 3rem; min-height: 3rem;
padding: 0.5rem 1.5rem; padding: 0.5rem 1.5rem;
gap: 0.75rem; gap: 0.75rem;
@@ -45,48 +167,22 @@
&--size-medium { &--size-medium {
font-weight: 600; font-weight: 600;
font-size: 0.75rem; font-size: 0.75rem;
height: 2.5rem; min-height: 2.5rem;
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
gap: 0.5rem; gap: 0.5rem;
letter-spacing: normal; letter-spacing: normal;
} }
&--variant-filled {
background: var(--accent-color);
border: 1px solid transparent;
&:hover {
background: var(--accent-color-hover);
}
&:active {
background: var(--accent-color-active);
}
}
&--variant-outlined,
&--variant-icon {
border: 1px solid var(--accent-color);
color: var(--accent-color);
background: transparent;
&:hover {
border: 1px solid var(--accent-color-hover);
color: var(--accent-color-hover);
}
&:active {
border: 1px solid var(--accent-color-active);
color: var(--accent-color-active);
}
}
&--variant-icon { &--variant-icon {
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;
width: 3rem; width: 3rem;
} }
&--fullWidth {
width: 100%;
}
&__icon { &__icon {
width: 1.25rem; width: 1.25rem;
height: 1.25rem; height: 1.25rem;
+4 -1
View File
@@ -4,7 +4,7 @@ import clsx from "clsx";
import "./FilledButton.scss"; import "./FilledButton.scss";
export type ButtonVariant = "filled" | "outlined" | "icon"; export type ButtonVariant = "filled" | "outlined" | "icon";
export type ButtonColor = "primary" | "danger"; export type ButtonColor = "primary" | "danger" | "warning" | "muted";
export type ButtonSize = "medium" | "large"; export type ButtonSize = "medium" | "large";
export type FilledButtonProps = { export type FilledButtonProps = {
@@ -17,6 +17,7 @@ export type FilledButtonProps = {
color?: ButtonColor; color?: ButtonColor;
size?: ButtonSize; size?: ButtonSize;
className?: string; className?: string;
fullWidth?: boolean;
startIcon?: React.ReactNode; startIcon?: React.ReactNode;
}; };
@@ -31,6 +32,7 @@ export const FilledButton = forwardRef<HTMLButtonElement, FilledButtonProps>(
variant = "filled", variant = "filled",
color = "primary", color = "primary",
size = "medium", size = "medium",
fullWidth,
className, className,
}, },
ref, ref,
@@ -42,6 +44,7 @@ export const FilledButton = forwardRef<HTMLButtonElement, FilledButtonProps>(
`ExcButton--color-${color}`, `ExcButton--color-${color}`,
`ExcButton--variant-${variant}`, `ExcButton--variant-${variant}`,
`ExcButton--size-${size}`, `ExcButton--size-${size}`,
{ "ExcButton--fullWidth": fullWidth },
className, className,
)} )}
onClick={onClick} onClick={onClick}
+1 -1
View File
@@ -12,7 +12,7 @@ const Header = () => (
<div className="HelpDialog__header"> <div className="HelpDialog__header">
<a <a
className="HelpDialog__btn" className="HelpDialog__btn"
href="https://github.com/excalidraw/excalidraw#documentation" href="https://docs.excalidraw.com"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
+13 -1
View File
@@ -41,6 +41,7 @@ import { jotaiScope } from "../jotai";
import { Provider, useAtom, useAtomValue } from "jotai"; import { Provider, useAtom, useAtomValue } from "jotai";
import MainMenu from "./main-menu/MainMenu"; import MainMenu from "./main-menu/MainMenu";
import { ActiveConfirmDialog } from "./ActiveConfirmDialog"; import { ActiveConfirmDialog } from "./ActiveConfirmDialog";
import { OverwriteConfirmDialog } from "./OverwriteConfirm/OverwriteConfirm";
import { HandButton } from "./HandButton"; import { HandButton } from "./HandButton";
import { isHandToolActive } from "../appState"; import { isHandToolActive } from "../appState";
import { TunnelsContext, useInitializeTunnels } from "../context/tunnels"; import { TunnelsContext, useInitializeTunnels } from "../context/tunnels";
@@ -99,6 +100,15 @@ const DefaultMainMenu: React.FC<{
); );
}; };
const DefaultOverwriteConfirmDialog = () => {
return (
<OverwriteConfirmDialog __fallback>
<OverwriteConfirmDialog.Actions.SaveToDisk />
<OverwriteConfirmDialog.Actions.ExportToImage />
</OverwriteConfirmDialog>
);
};
const LayerUI = ({ const LayerUI = ({
actionManager, actionManager,
appState, appState,
@@ -343,6 +353,7 @@ const LayerUI = ({
> >
{t("toolBar.library")} {t("toolBar.library")}
</DefaultSidebar.Trigger> </DefaultSidebar.Trigger>
<DefaultOverwriteConfirmDialog />
{/* ------------------------------------------------------------------ */} {/* ------------------------------------------------------------------ */}
{appState.isLoading && <LoadingMessage delay={250} />} {appState.isLoading && <LoadingMessage delay={250} />}
@@ -374,6 +385,7 @@ const LayerUI = ({
/> />
)} )}
<ActiveConfirmDialog /> <ActiveConfirmDialog />
<tunnels.OverwriteConfirmDialogTunnel.Out />
{renderImageExportDialog()} {renderImageExportDialog()}
{renderJSONExportDialog()} {renderJSONExportDialog()}
{appState.pasteDialog.shown && ( {appState.pasteDialog.shown && (
@@ -387,7 +399,7 @@ const LayerUI = ({
} }
/> />
)} )}
{device.isMobile && !eyeDropperState && ( {device.isMobile && (
<MobileMenu <MobileMenu
appState={appState} appState={appState}
elements={elements} elements={elements}
+1 -1
View File
@@ -3,7 +3,7 @@
.excalidraw { .excalidraw {
&.excalidraw-modal-container { &.excalidraw-modal-container {
position: absolute; position: absolute;
z-index: 10; z-index: var(--zIndex-modal);
} }
.Modal { .Modal {
@@ -0,0 +1,126 @@
@import "../../css/variables.module";
.excalidraw {
.OverwriteConfirm {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.75rem;
isolation: isolate;
h3 {
margin: 0;
font-weight: 700;
font-size: 1.3125rem;
line-height: 130%;
align-self: flex-start;
color: var(--text-primary-color);
}
&__Description {
box-sizing: border-box;
display: flex;
flex-direction: row;
align-items: center;
width: 100%;
gap: 1rem;
@include isMobile {
flex-direction: column;
text-align: center;
}
padding: 2.5rem;
background: var(--color-danger-background);
border-radius: 0.5rem;
font-family: "Assistant";
font-style: normal;
font-weight: 400;
font-size: 1rem;
line-height: 150%;
color: var(--color-danger-color);
&__spacer {
flex-grow: 1;
}
&__icon {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
border-radius: 2.5rem;
background: var(--color-danger-icon-background);
width: 3.5rem;
height: 3.5rem;
padding: 0.75rem;
svg {
color: var(--color-danger-icon-color);
width: 1.5rem;
height: 1.5rem;
}
}
&.OverwriteConfirm__Description--color-warning {
background: var(--color-warning-background);
color: var(--color-warning-color);
.OverwriteConfirm__Description__icon {
background: var(--color-warning-icon-background);
flex: 0 0 auto;
svg {
color: var(--color-warning-icon-color);
}
}
}
}
&__Actions {
display: flex;
flex-direction: row;
align-items: stretch;
justify-items: stretch;
justify-content: center;
gap: 1.5rem;
@include isMobile {
flex-direction: column;
}
&__Action {
display: flex;
flex-direction: column;
align-items: center;
padding: 1.5rem;
gap: 0.75rem;
flex-basis: 50%;
flex-grow: 0;
&__content {
height: 100%;
font-size: 0.875rem;
text-align: center;
}
h4 {
font-weight: 700;
font-size: 1.125rem;
line-height: 130%;
margin: 0;
color: var(--text-primary-color);
}
}
}
}
}
@@ -0,0 +1,76 @@
import React from "react";
import { useAtom } from "jotai";
import { useTunnels } from "../../context/tunnels";
import { jotaiScope } from "../../jotai";
import { Dialog } from "../Dialog";
import { withInternalFallback } from "../hoc/withInternalFallback";
import { overwriteConfirmStateAtom } from "./OverwriteConfirmState";
import { FilledButton } from "../FilledButton";
import { alertTriangleIcon } from "../icons";
import { Actions, Action } from "./OverwriteConfirmActions";
import "./OverwriteConfirm.scss";
export type OverwriteConfirmDialogProps = {
children: React.ReactNode;
};
const OverwriteConfirmDialog = Object.assign(
withInternalFallback(
"OverwriteConfirmDialog",
({ children }: OverwriteConfirmDialogProps) => {
const { OverwriteConfirmDialogTunnel } = useTunnels();
const [overwriteConfirmState, setState] = useAtom(
overwriteConfirmStateAtom,
jotaiScope,
);
if (!overwriteConfirmState.active) {
return null;
}
const handleClose = () => {
overwriteConfirmState.onClose();
setState((state) => ({ ...state, active: false }));
};
const handleConfirm = () => {
overwriteConfirmState.onConfirm();
setState((state) => ({ ...state, active: false }));
};
return (
<OverwriteConfirmDialogTunnel.In>
<Dialog onCloseRequest={handleClose} title={false} size={916}>
<div className="OverwriteConfirm">
<h3>{overwriteConfirmState.title}</h3>
<div
className={`OverwriteConfirm__Description OverwriteConfirm__Description--color-${overwriteConfirmState.color}`}
>
<div className="OverwriteConfirm__Description__icon">
{alertTriangleIcon}
</div>
<div>{overwriteConfirmState.description}</div>
<div className="OverwriteConfirm__Description__spacer"></div>
<FilledButton
color={overwriteConfirmState.color}
size="large"
label={overwriteConfirmState.actionLabel}
onClick={handleConfirm}
/>
</div>
<Actions>{children}</Actions>
</div>
</Dialog>
</OverwriteConfirmDialogTunnel.In>
);
},
),
{
Actions,
Action,
},
);
export { OverwriteConfirmDialog };
@@ -0,0 +1,85 @@
import React from "react";
import { FilledButton } from "../FilledButton";
import { useExcalidrawActionManager, useExcalidrawSetAppState } from "../App";
import { actionSaveFileToDisk } from "../../actions";
import { useI18n } from "../../i18n";
import { actionChangeExportEmbedScene } from "../../actions/actionExport";
export type ActionProps = {
title: string;
children: React.ReactNode;
actionLabel: string;
onClick: () => void;
};
export const Action = ({
title,
children,
actionLabel,
onClick,
}: ActionProps) => {
return (
<div className="OverwriteConfirm__Actions__Action">
<h4>{title}</h4>
<div className="OverwriteConfirm__Actions__Action__content">
{children}
</div>
<FilledButton
variant="outlined"
color="muted"
label={actionLabel}
size="large"
fullWidth
onClick={onClick}
/>
</div>
);
};
export const ExportToImage = () => {
const { t } = useI18n();
const actionManager = useExcalidrawActionManager();
const setAppState = useExcalidrawSetAppState();
return (
<Action
title={t("overwriteConfirm.action.exportToImage.title")}
actionLabel={t("overwriteConfirm.action.exportToImage.button")}
onClick={() => {
actionManager.executeAction(actionChangeExportEmbedScene, "ui", true);
setAppState({ openDialog: "imageExport" });
}}
>
{t("overwriteConfirm.action.exportToImage.description")}
</Action>
);
};
export const SaveToDisk = () => {
const { t } = useI18n();
const actionManager = useExcalidrawActionManager();
return (
<Action
title={t("overwriteConfirm.action.saveToDisk.title")}
actionLabel={t("overwriteConfirm.action.saveToDisk.button")}
onClick={() => {
actionManager.executeAction(actionSaveFileToDisk, "ui");
}}
>
{t("overwriteConfirm.action.saveToDisk.description")}
</Action>
);
};
const Actions = Object.assign(
({ children }: { children: React.ReactNode }) => {
return <div className="OverwriteConfirm__Actions">{children}</div>;
},
{
ExportToImage,
SaveToDisk,
},
);
export { Actions };
@@ -0,0 +1,46 @@
import { atom } from "jotai";
import { jotaiStore } from "../../jotai";
import React from "react";
export type OverwriteConfirmState =
| {
active: true;
title: string;
description: React.ReactNode;
actionLabel: string;
color: "danger" | "warning";
onClose: () => void;
onConfirm: () => void;
onReject: () => void;
}
| { active: false };
export const overwriteConfirmStateAtom = atom<OverwriteConfirmState>({
active: false,
});
export async function openConfirmModal({
title,
description,
actionLabel,
color,
}: {
title: string;
description: React.ReactNode;
actionLabel: string;
color: "danger" | "warning";
}) {
return new Promise<boolean>((resolve) => {
jotaiStore.set(overwriteConfirmStateAtom, {
active: true,
onConfirm: () => resolve(true),
onClose: () => resolve(false),
onReject: () => resolve(false),
title,
description,
actionLabel,
color,
});
});
}
+91
View File
@@ -0,0 +1,91 @@
@import "../css/variables.module";
.excalidraw {
.ShareableLinkDialog {
display: flex;
flex-direction: column;
gap: 1.5rem;
color: var(--text-primary-color);
::selection {
background: var(--color-primary-light-darker);
}
h3 {
font-family: "Assistant";
font-weight: 700;
font-size: 1.313rem;
line-height: 130%;
margin: 0;
}
&__popover {
@keyframes RoomDialog__popover__scaleIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
box-sizing: border-box;
z-index: 100;
display: flex;
flex-direction: row;
justify-content: center;
align-items: flex-start;
padding: 0.125rem 0.5rem;
gap: 0.125rem;
height: 1.125rem;
border: none;
border-radius: 0.6875rem;
font-family: "Assistant";
font-style: normal;
font-weight: 600;
font-size: 0.75rem;
line-height: 110%;
background: var(--color-success-lighter);
color: var(--color-success);
& > svg {
width: 0.875rem;
height: 0.875rem;
}
transform-origin: var(--radix-popover-content-transform-origin);
animation: RoomDialog__popover__scaleIn 150ms ease-out;
}
&__linkRow {
display: flex;
flex-direction: row;
align-items: flex-end;
gap: 0.75rem;
}
&__description {
border-top: 1px solid var(--color-gray-20);
padding: 0.5rem 0.5rem 0;
font-weight: 400;
font-size: 0.75rem;
line-height: 150%;
& p {
margin: 0;
}
& p + p {
margin-top: 1em;
}
}
}
}
+91
View File
@@ -0,0 +1,91 @@
import { useRef, useState } from "react";
import * as Popover from "@radix-ui/react-popover";
import { copyTextToSystemClipboard } from "../clipboard";
import { useI18n } from "../i18n";
import { Dialog } from "./Dialog";
import { TextField } from "./TextField";
import { FilledButton } from "./FilledButton";
import { copyIcon, tablerCheckIcon } from "./icons";
import "./ShareableLinkDialog.scss";
export type ShareableLinkDialogProps = {
link: string;
onCloseRequest: () => void;
setErrorMessage: (error: string) => void;
};
export const ShareableLinkDialog = ({
link,
onCloseRequest,
setErrorMessage,
}: ShareableLinkDialogProps) => {
const { t } = useI18n();
const [justCopied, setJustCopied] = useState(false);
const timerRef = useRef<number>(0);
const ref = useRef<HTMLInputElement>(null);
const copyRoomLink = async () => {
try {
await copyTextToSystemClipboard(link);
setJustCopied(true);
if (timerRef.current) {
window.clearTimeout(timerRef.current);
}
timerRef.current = window.setTimeout(() => {
setJustCopied(false);
}, 3000);
} catch (error: any) {
setErrorMessage(error.message);
}
ref.current?.select();
};
return (
<Dialog onCloseRequest={onCloseRequest} title={false} size="small">
<div className="ShareableLinkDialog">
<h3>Shareable link</h3>
<div className="ShareableLinkDialog__linkRow">
<TextField
ref={ref}
label="Link"
readonly
fullWidth
value={link}
selectOnRender
/>
<Popover.Root open={justCopied}>
<Popover.Trigger asChild>
<FilledButton
size="large"
label="Copy link"
startIcon={copyIcon}
onClick={copyRoomLink}
/>
</Popover.Trigger>
<Popover.Content
onOpenAutoFocus={(event) => event.preventDefault()}
onCloseAutoFocus={(event) => event.preventDefault()}
className="ShareableLinkDialog__popover"
side="top"
align="end"
sideOffset={5.5}
>
{tablerCheckIcon} copied
</Popover.Content>
</Popover.Root>
</div>
<div className="ShareableLinkDialog__description">
🔒 {t("alerts.uploadedSecurly")}
</div>
</div>
</Dialog>
);
};
+24 -2
View File
@@ -1,4 +1,10 @@
import { forwardRef, useRef, useImperativeHandle, KeyboardEvent } from "react"; import {
forwardRef,
useRef,
useImperativeHandle,
KeyboardEvent,
useLayoutEffect,
} from "react";
import clsx from "clsx"; import clsx from "clsx";
import "./TextField.scss"; import "./TextField.scss";
@@ -12,6 +18,7 @@ export type TextFieldProps = {
readonly?: boolean; readonly?: boolean;
fullWidth?: boolean; fullWidth?: boolean;
selectOnRender?: boolean;
label?: string; label?: string;
placeholder?: string; placeholder?: string;
@@ -19,13 +26,28 @@ export type TextFieldProps = {
export const TextField = forwardRef<HTMLInputElement, TextFieldProps>( export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
( (
{ value, onChange, label, fullWidth, placeholder, readonly, onKeyDown }, {
value,
onChange,
label,
fullWidth,
placeholder,
readonly,
selectOnRender,
onKeyDown,
},
ref, ref,
) => { ) => {
const innerRef = useRef<HTMLInputElement | null>(null); const innerRef = useRef<HTMLInputElement | null>(null);
useImperativeHandle(ref, () => innerRef.current!); useImperativeHandle(ref, () => innerRef.current!);
useLayoutEffect(() => {
if (selectOnRender) {
innerRef.current?.select();
}
}, [selectOnRender]);
return ( return (
<div <div
className={clsx("ExcTextField", { className={clsx("ExcTextField", {
+1 -1
View File
@@ -6,7 +6,7 @@
Roboto, Helvetica, Arial, sans-serif; Roboto, Helvetica, Arial, sans-serif;
font-family: var(--ui-font); font-family: var(--ui-font);
position: fixed; position: fixed;
z-index: 1000; z-index: var(--zIndex-popup);
padding: 8px; padding: 8px;
border-radius: 6px; border-radius: 6px;
+21 -4
View File
@@ -1608,13 +1608,30 @@ export const tablerCheckIcon = createIcon(
tablerIconProps, tablerIconProps,
); );
export const alertTriangleIcon = createIcon(
<>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M10.24 3.957l-8.422 14.06a1.989 1.989 0 0 0 1.7 2.983h16.845a1.989 1.989 0 0 0 1.7 -2.983l-8.423 -14.06a1.989 1.989 0 0 0 -3.4 0z" />
<path d="M12 9v4" />
<path d="M12 17h.01" />
</>,
tablerIconProps,
);
export const eyeDropperIcon = createIcon( export const eyeDropperIcon = createIcon(
<g strokeWidth={1.25}> <g strokeWidth={1.25}>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="m9.168 5.833 5 5M9.999 6.667l-6.667 6.666v3.334h3.333L13.332 10M9.999 6.667l3.083-3.084a.833.833 0 0 1 1.167 0l2.166 2.167a.833.833 0 0 1 0 1.167L13.332 10M9.999 6.667 13.332 10" />
<path d="M11 7l6 6"></path> <rect
<path d="M4 16l11.7 -11.7a1 1 0 0 1 1.4 0l2.6 2.6a1 1 0 0 1 0 1.4l-11.7 11.7h-4v-4z"></path> x="13.684"
y="2.976"
width="4.751"
height="5.106"
rx="1"
transform="rotate(45 13.684 2.976)"
fill="currentColor"
/>
</g>, </g>,
tablerIconProps, modifiedTablerIconProps,
); );
export const extraToolsIcon = createIcon( export const extraToolsIcon = createIcon(
+29 -2
View File
@@ -1,6 +1,10 @@
import { getShortcutFromShortcutName } from "../../actions/shortcuts"; import { getShortcutFromShortcutName } from "../../actions/shortcuts";
import { useI18n } from "../../i18n"; import { useI18n } from "../../i18n";
import { useExcalidrawSetAppState, useExcalidrawActionManager } from "../App"; import {
useExcalidrawSetAppState,
useExcalidrawActionManager,
useExcalidrawElements,
} from "../App";
import { import {
ExportIcon, ExportIcon,
ExportImageIcon, ExportImageIcon,
@@ -29,19 +33,42 @@ import { useSetAtom } from "jotai";
import { activeConfirmDialogAtom } from "../ActiveConfirmDialog"; import { activeConfirmDialogAtom } from "../ActiveConfirmDialog";
import { jotaiScope } from "../../jotai"; import { jotaiScope } from "../../jotai";
import { useUIAppState } from "../../context/ui-appState"; import { useUIAppState } from "../../context/ui-appState";
import { openConfirmModal } from "../OverwriteConfirm/OverwriteConfirmState";
import Trans from "../Trans";
export const LoadScene = () => { export const LoadScene = () => {
const { t } = useI18n(); const { t } = useI18n();
const actionManager = useExcalidrawActionManager(); const actionManager = useExcalidrawActionManager();
const elements = useExcalidrawElements();
if (!actionManager.isActionEnabled(actionLoadScene)) { if (!actionManager.isActionEnabled(actionLoadScene)) {
return null; return null;
} }
const handleSelect = async () => {
if (
!elements.length ||
(await openConfirmModal({
title: t("overwriteConfirm.modal.loadFromFile.title"),
actionLabel: t("overwriteConfirm.modal.loadFromFile.button"),
color: "warning",
description: (
<Trans
i18nKey="overwriteConfirm.modal.loadFromFile.description"
bold={(text) => <strong>{text}</strong>}
br={() => <br />}
/>
),
}))
) {
actionManager.executeAction(actionLoadScene);
}
};
return ( return (
<DropdownMenuItem <DropdownMenuItem
icon={LoadIcon} icon={LoadIcon}
onSelect={() => actionManager.executeAction(actionLoadScene)} onSelect={handleSelect}
data-testid="load-button" data-testid="load-button"
shortcut={getShortcutFromShortcutName("loadScene")} shortcut={getShortcutFromShortcutName("loadScene")}
aria-label={t("buttons.load")} aria-label={t("buttons.load")}
+2
View File
@@ -12,6 +12,7 @@ type TunnelsContextValue = {
FooterCenterTunnel: Tunnel; FooterCenterTunnel: Tunnel;
DefaultSidebarTriggerTunnel: Tunnel; DefaultSidebarTriggerTunnel: Tunnel;
DefaultSidebarTabTriggersTunnel: Tunnel; DefaultSidebarTabTriggersTunnel: Tunnel;
OverwriteConfirmDialogTunnel: Tunnel;
jotaiScope: symbol; jotaiScope: symbol;
}; };
@@ -30,6 +31,7 @@ export const useInitializeTunnels = () => {
FooterCenterTunnel: tunnel(), FooterCenterTunnel: tunnel(),
DefaultSidebarTriggerTunnel: tunnel(), DefaultSidebarTriggerTunnel: tunnel(),
DefaultSidebarTabTriggersTunnel: tunnel(), DefaultSidebarTabTriggersTunnel: tunnel(),
OverwriteConfirmDialogTunnel: tunnel(),
jotaiScope: Symbol(), jotaiScope: Symbol(),
}; };
}, []); }, []);
+9
View File
@@ -5,6 +5,15 @@
--zIndex-canvas: 1; --zIndex-canvas: 1;
--zIndex-wysiwyg: 2; --zIndex-wysiwyg: 2;
--zIndex-layerUI: 3; --zIndex-layerUI: 3;
--zIndex-modal: 1000;
--zIndex-popup: 1001;
--zIndex-toast: 999999;
--sab: env(safe-area-inset-bottom);
--sal: env(safe-area-inset-left);
--sar: env(safe-area-inset-right);
--sat: env(safe-area-inset-top);
} }
.excalidraw { .excalidraw {
+45 -4
View File
@@ -27,10 +27,6 @@
--popup-secondary-bg-color: #{$oc-gray-1}; --popup-secondary-bg-color: #{$oc-gray-1};
--popup-text-color: #{$oc-black}; --popup-text-color: #{$oc-black};
--popup-text-inverted-color: #{$oc-white}; --popup-text-inverted-color: #{$oc-white};
--sab: env(safe-area-inset-bottom);
--sal: env(safe-area-inset-left);
--sar: env(safe-area-inset-right);
--sat: env(safe-area-inset-top);
--select-highlight-color: #{$oc-blue-5}; --select-highlight-color: #{$oc-blue-5};
--shadow-island: 0px 7px 14px rgba(0, 0, 0, 0.05), --shadow-island: 0px 7px 14px rgba(0, 0, 0, 0.05),
0px 0px 3.12708px rgba(0, 0, 0, 0.0798), 0px 0px 3.12708px rgba(0, 0, 0, 0.0798),
@@ -99,9 +95,33 @@
--color-gray-100: #121212; --color-gray-100: #121212;
--color-warning: #fceeca; --color-warning: #fceeca;
--color-warning-dark: #f5c354;
--color-warning-darker: #f3ab2c;
--color-warning-darkest: #ec8b14;
--color-text-warning: var(--text-primary-color); --color-text-warning: var(--text-primary-color);
--color-danger: #db6965; --color-danger: #db6965;
--color-danger-dark: #db6965;
--color-danger-darker: #d65550;
--color-danger-darkest: #d1413c;
--color-danger-text: black;
--color-danger-background: #fff0f0;
--color-danger-icon-background: #ffdad6;
--color-danger-color: #700000;
--color-danger-icon-color: #700000;
--color-warning-background: var(--color-warning);
--color-warning-icon-background: var(--color-warning-dark);
--color-warning-color: var(--text-primary-color);
--color-warning-icon-color: var(--text-primary-color);
--color-muted: var(--color-gray-30);
--color-muted-darker: var(--color-gray-60);
--color-muted-darkest: var(--color-gray-100);
--color-muted-background: var(--color-gray-80);
--color-muted-background-darker: var(--color-gray-100);
--color-promo: #e70078; --color-promo: #e70078;
--color-success: #268029; --color-success: #268029;
--color-success-lighter: #cafccc; --color-success-lighter: #cafccc;
@@ -177,6 +197,27 @@
--color-text-warning: var(--color-gray-80); --color-text-warning: var(--color-gray-80);
--color-danger: #ffa8a5; --color-danger: #ffa8a5;
--color-danger-dark: #672120;
--color-danger-darker: #8f2625;
--color-danger-darkest: #ac2b29;
--color-danger-text: #fbcbcc;
--color-danger-background: #fbcbcc;
--color-danger-icon-background: #672120;
--color-danger-color: #261919;
--color-danger-icon-color: #fbcbcc;
--color-warning-background: var(--color-warning);
--color-warning-icon-background: var(--color-warning-dark);
--color-warning-color: var(--color-gray-80);
--color-warning-icon-color: var(--color-gray-80);
--color-muted: var(--color-gray-80);
--color-muted-darker: var(--color-gray-60);
--color-muted-darkest: var(--color-gray-20);
--color-muted-background: var(--color-gray-40);
--color-muted-background-darker: var(--color-gray-20);
--color-promo: #d297ff; --color-promo: #d297ff;
} }
} }
+24 -1
View File
@@ -41,6 +41,7 @@ import {
measureBaseline, measureBaseline,
} from "../element/textElement"; } from "../element/textElement";
import { COLOR_PALETTE } from "../colors"; import { COLOR_PALETTE } from "../colors";
import { normalizeLink } from "./url";
type RestoredAppState = Omit< type RestoredAppState = Omit<
AppState, AppState,
@@ -142,7 +143,7 @@ const restoreElementWithProperties = <
? element.boundElementIds.map((id) => ({ type: "arrow", id })) ? element.boundElementIds.map((id) => ({ type: "arrow", id }))
: element.boundElements ?? [], : element.boundElements ?? [],
updated: element.updated ?? getUpdatedTimestamp(), updated: element.updated ?? getUpdatedTimestamp(),
link: element.link ?? null, link: element.link ? normalizeLink(element.link) : null,
locked: element.locked ?? false, locked: element.locked ?? false,
}; };
@@ -370,6 +371,24 @@ const repairBoundElement = (
} }
}; };
/**
* Remove an element's frameId if its containing frame is non-existent
*
* NOTE mutates elements.
*/
const repairFrameMembership = (
element: Mutable<ExcalidrawElement>,
elementsMap: Map<string, Mutable<ExcalidrawElement>>,
) => {
if (element.frameId) {
const containingFrame = elementsMap.get(element.frameId);
if (!containingFrame) {
element.frameId = null;
}
}
};
export const restoreElements = ( export const restoreElements = (
elements: ImportedDataState["elements"], elements: ImportedDataState["elements"],
/** NOTE doesn't serve for reconciliation */ /** NOTE doesn't serve for reconciliation */
@@ -410,6 +429,10 @@ export const restoreElements = (
// repair binding. Mutates elements. // repair binding. Mutates elements.
const restoredElementsMap = arrayToMap(restoredElements); const restoredElementsMap = arrayToMap(restoredElements);
for (const element of restoredElements) { for (const element of restoredElements) {
if (element.frameId) {
repairFrameMembership(element, restoredElementsMap);
}
if (isTextElement(element) && element.containerId) { if (isTextElement(element) && element.containerId) {
repairBoundElement(element, restoredElementsMap); repairBoundElement(element, restoredElementsMap);
} else if (element.boundElements) { } else if (element.boundElements) {
+30
View File
@@ -0,0 +1,30 @@
import { normalizeLink } from "./url";
describe("normalizeLink", () => {
// NOTE not an extensive XSS test suite, just to check if we're not
// regressing in sanitization
it("should sanitize links", () => {
expect(
// eslint-disable-next-line no-script-url
normalizeLink(`javascript://%0aalert(document.domain)`).startsWith(
// eslint-disable-next-line no-script-url
`javascript:`,
),
).toBe(false);
expect(normalizeLink("ola")).toBe("ola");
expect(normalizeLink(" ola")).toBe("ola");
expect(normalizeLink("https://www.excalidraw.com")).toBe(
"https://www.excalidraw.com",
);
expect(normalizeLink("www.excalidraw.com")).toBe("www.excalidraw.com");
expect(normalizeLink("/ola")).toBe("/ola");
expect(normalizeLink("http://test")).toBe("http://test");
expect(normalizeLink("ftp://test")).toBe("ftp://test");
expect(normalizeLink("file://")).toBe("file://");
expect(normalizeLink("file://")).toBe("file://");
expect(normalizeLink("[test](https://test)")).toBe("[test](https://test)");
expect(normalizeLink("[[test]]")).toBe("[[test]]");
expect(normalizeLink("<test>")).toBe("<test>");
});
});
+9
View File
@@ -0,0 +1,9 @@
import { sanitizeUrl } from "@braintree/sanitize-url";
export const normalizeLink = (link: string) => {
return sanitizeUrl(link);
};
export const isLocalLink = (link: string | null) => {
return !!(link?.includes(location.origin) || link?.startsWith("/"));
};
+9 -17
View File
@@ -29,6 +29,7 @@ import { getTooltipDiv, updateTooltipPosition } from "../components/Tooltip";
import { getSelectedElements } from "../scene"; import { getSelectedElements } from "../scene";
import { isPointHittingElementBoundingBox } from "./collision"; import { isPointHittingElementBoundingBox } from "./collision";
import { getElementAbsoluteCoords } from "./"; import { getElementAbsoluteCoords } from "./";
import { isLocalLink, normalizeLink } from "../data/url";
import "./Hyperlink.scss"; import "./Hyperlink.scss";
import { trackEvent } from "../analytics"; import { trackEvent } from "../analytics";
@@ -166,7 +167,7 @@ export const Hyperlink = ({
/> />
) : ( ) : (
<a <a
href={element.link || ""} href={normalizeLink(element.link || "")}
className={clsx("excalidraw-hyperlinkContainer-link", { className={clsx("excalidraw-hyperlinkContainer-link", {
"d-none": isEditing, "d-none": isEditing,
})} })}
@@ -177,7 +178,13 @@ export const Hyperlink = ({
EVENT.EXCALIDRAW_LINK, EVENT.EXCALIDRAW_LINK,
event.nativeEvent, event.nativeEvent,
); );
onLinkOpen(element, customEvent); onLinkOpen(
{
...element,
link: normalizeLink(element.link),
},
customEvent,
);
if (customEvent.defaultPrevented) { if (customEvent.defaultPrevented) {
event.preventDefault(); event.preventDefault();
} }
@@ -231,21 +238,6 @@ const getCoordsForPopover = (
return { x, y }; return { x, y };
}; };
export const normalizeLink = (link: string) => {
link = link.trim();
if (link) {
// prefix with protocol if not fully-qualified
if (!link.includes("://") && !/^[[\\/]/.test(link)) {
link = `https://${link}`;
}
}
return link;
};
export const isLocalLink = (link: string | null) => {
return !!(link?.includes(location.origin) || link?.startsWith("/"));
};
export const actionLink = register({ export const actionLink = register({
name: "hyperlink", name: "hyperlink",
perform: (elements, appState) => { perform: (elements, appState) => {
-1
View File
@@ -862,7 +862,6 @@ const VALID_CONTAINER_TYPES = new Set([
"rectangle", "rectangle",
"ellipse", "ellipse",
"diamond", "diamond",
"image",
"arrow", "arrow",
]); ]);
+49
View File
@@ -1459,5 +1459,54 @@ describe("textWysiwyg", () => {
}), }),
); );
}); });
it("shouldn't bind to container if container has bound text not centered and text tool is used", async () => {
expect(h.elements.length).toBe(1);
Keyboard.keyPress(KEYS.ENTER);
expect(h.elements.length).toBe(2);
// Bind first text
let text = h.elements[1] as ExcalidrawTextElementWithContainer;
expect(text.containerId).toBe(rectangle.id);
let editor = getTextEditor();
await new Promise((r) => setTimeout(r, 0));
updateTextEditor(editor, "Hello!");
expect(
(h.elements[1] as ExcalidrawTextElementWithContainer).verticalAlign,
).toBe(VERTICAL_ALIGN.MIDDLE);
fireEvent.click(screen.getByTitle("Align bottom"));
await new Promise((r) => setTimeout(r, 0));
editor.blur();
expect(rectangle.boundElements).toStrictEqual([
{ id: text.id, type: "text" },
]);
expect(
(h.elements[1] as ExcalidrawTextElementWithContainer).verticalAlign,
).toBe(VERTICAL_ALIGN.BOTTOM);
// Attempt to Bind 2nd text using text tool
UI.clickTool("text");
mouse.clickAt(
rectangle.x + rectangle.width / 2,
rectangle.y + rectangle.height / 2,
);
editor = getTextEditor();
await new Promise((r) => setTimeout(r, 0));
updateTextEditor(editor, "Excalidraw");
editor.blur();
expect(h.elements.length).toBe(3);
expect(rectangle.boundElements).toStrictEqual([
{ id: h.elements[1].id, type: "text" },
]);
text = h.elements[2] as ExcalidrawTextElementWithContainer;
expect(text.containerId).toBe(null);
expect(text.text).toBe("Excalidraw");
});
}); });
}); });
+9 -9
View File
@@ -30,15 +30,6 @@ describe("Test TypeChecks", () => {
}), }),
), ),
).toBeTruthy(); ).toBeTruthy();
expect(
hasBoundTextElement(
API.createElement({
type: "image",
boundElements: [{ type: "text", id: "text-id" }],
}),
),
).toBeTruthy();
}); });
it("should return false for text bindable containers without bound text", () => { it("should return false for text bindable containers without bound text", () => {
@@ -62,5 +53,14 @@ describe("Test TypeChecks", () => {
), ),
).toBeFalsy(); ).toBeFalsy();
}); });
expect(
hasBoundTextElement(
API.createElement({
type: "image",
boundElements: [{ type: "text", id: "text-id" }],
}),
),
).toBeFalsy();
}); });
}); });
-1
View File
@@ -126,7 +126,6 @@ export const isTextBindableContainer = (
(element.type === "rectangle" || (element.type === "rectangle" ||
element.type === "diamond" || element.type === "diamond" ||
element.type === "ellipse" || element.type === "ellipse" ||
element.type === "image" ||
isArrowElement(element)) isArrowElement(element))
); );
}; };
-1
View File
@@ -162,7 +162,6 @@ export type ExcalidrawTextContainer =
| ExcalidrawRectangleElement | ExcalidrawRectangleElement
| ExcalidrawDiamondElement | ExcalidrawDiamondElement
| ExcalidrawEllipseElement | ExcalidrawEllipseElement
| ExcalidrawImageElement
| ExcalidrawArrowElement; | ExcalidrawArrowElement;
export type ExcalidrawTextElementWithContainer = { export type ExcalidrawTextElementWithContainer = {
@@ -16,7 +16,7 @@ import { MIME_TYPES } from "../../constants";
import { trackEvent } from "../../analytics"; import { trackEvent } from "../../analytics";
import { getFrame } from "../../utils"; import { getFrame } from "../../utils";
const exportToExcalidrawPlus = async ( export const exportToExcalidrawPlus = async (
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],
appState: Partial<AppState>, appState: Partial<AppState>,
files: BinaryFiles, files: BinaryFiles,
+14 -6
View File
@@ -282,11 +282,15 @@ export const loadScene = async (
}; };
}; };
type ExportToBackendResult =
| { url: null; errorMessage: string }
| { url: string; errorMessage: null };
export const exportToBackend = async ( export const exportToBackend = async (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
appState: Partial<AppState>, appState: Partial<AppState>,
files: BinaryFiles, files: BinaryFiles,
) => { ): Promise<ExportToBackendResult> => {
const encryptionKey = await generateEncryptionKey("string"); const encryptionKey = await generateEncryptionKey("string");
const payload = await compressData( const payload = await compressData(
@@ -327,14 +331,18 @@ export const exportToBackend = async (
files: filesToUpload, files: filesToUpload,
}); });
window.prompt(`🔒${t("alerts.uploadedSecurly")}`, urlString); return { url: urlString, errorMessage: null };
} else if (json.error_class === "RequestTooLargeError") { } else if (json.error_class === "RequestTooLargeError") {
window.alert(t("alerts.couldNotCreateShareableLinkTooBig")); return {
} else { url: null,
window.alert(t("alerts.couldNotCreateShareableLink")); errorMessage: t("alerts.couldNotCreateShareableLinkTooBig"),
};
} }
return { url: null, errorMessage: t("alerts.couldNotCreateShareableLink") };
} catch (error: any) { } catch (error: any) {
console.error(error); console.error(error);
window.alert(t("alerts.couldNotCreateShareableLink"));
return { url: null, errorMessage: t("alerts.couldNotCreateShareableLink") };
} }
}; };
+67 -9
View File
@@ -69,7 +69,10 @@ import {
} from "./data/localStorage"; } from "./data/localStorage";
import CustomStats from "./CustomStats"; import CustomStats from "./CustomStats";
import { restore, restoreAppState, RestoredDataState } from "../data/restore"; import { restore, restoreAppState, RestoredDataState } from "../data/restore";
import { ExportToExcalidrawPlus } from "./components/ExportToExcalidrawPlus"; import {
ExportToExcalidrawPlus,
exportToExcalidrawPlus,
} from "./components/ExportToExcalidrawPlus";
import { updateStaleImageStatuses } from "./data/FileManager"; import { updateStaleImageStatuses } from "./data/FileManager";
import { newElementWith } from "../element/mutateElement"; import { newElementWith } from "../element/mutateElement";
import { isInitializedImageElement } from "../element/typeChecks"; import { isInitializedImageElement } from "../element/typeChecks";
@@ -88,6 +91,10 @@ import { appJotaiStore } from "./app-jotai";
import "./index.scss"; import "./index.scss";
import { ResolutionType } from "../utility-types"; import { ResolutionType } from "../utility-types";
import { ShareableLinkDialog } from "../components/ShareableLinkDialog";
import { openConfirmModal } from "../components/OverwriteConfirm/OverwriteConfirmState";
import { OverwriteConfirmDialog } from "../components/OverwriteConfirm/OverwriteConfirm";
import Trans from "../components/Trans";
polyfill(); polyfill();
@@ -98,6 +105,19 @@ languageDetector.init({
languageUtils: {}, languageUtils: {},
}); });
const shareableLinkConfirmDialog = {
title: t("overwriteConfirm.modal.shareableLink.title"),
description: (
<Trans
i18nKey="overwriteConfirm.modal.shareableLink.description"
bold={(text) => <strong>{text}</strong>}
br={() => <br />}
/>
),
actionLabel: t("overwriteConfirm.modal.shareableLink.button"),
color: "danger",
} as const;
const initializeScene = async (opts: { const initializeScene = async (opts: {
collabAPI: CollabAPI | null; collabAPI: CollabAPI | null;
excalidrawAPI: ExcalidrawImperativeAPI; excalidrawAPI: ExcalidrawImperativeAPI;
@@ -129,7 +149,7 @@ const initializeScene = async (opts: {
// don't prompt for collab scenes because we don't override local storage // don't prompt for collab scenes because we don't override local storage
roomLinkData || roomLinkData ||
// otherwise, prompt whether user wants to override current scene // otherwise, prompt whether user wants to override current scene
window.confirm(t("alerts.loadSceneOverridePrompt")) (await openConfirmModal(shareableLinkConfirmDialog))
) { ) {
if (jsonBackendMatch) { if (jsonBackendMatch) {
scene = await loadScene( scene = await loadScene(
@@ -168,7 +188,7 @@ const initializeScene = async (opts: {
const data = await loadFromBlob(await request.blob(), null, null); const data = await loadFromBlob(await request.blob(), null, null);
if ( if (
!scene.elements.length || !scene.elements.length ||
window.confirm(t("alerts.loadSceneOverridePrompt")) (await openConfirmModal(shareableLinkConfirmDialog))
) { ) {
return { scene: data, isExternalScene }; return { scene: data, isExternalScene };
} }
@@ -554,6 +574,10 @@ const ExcalidrawWrapper = () => {
} }
}; };
const [latestShareableLink, setLatestShareableLink] = useState<string | null>(
null,
);
const onExportToBackend = async ( const onExportToBackend = async (
exportedElements: readonly NonDeletedExcalidrawElement[], exportedElements: readonly NonDeletedExcalidrawElement[],
appState: Partial<AppState>, appState: Partial<AppState>,
@@ -565,7 +589,7 @@ const ExcalidrawWrapper = () => {
} }
if (canvas) { if (canvas) {
try { try {
await exportToBackend( const { url, errorMessage } = await exportToBackend(
exportedElements, exportedElements,
{ {
...appState, ...appState,
@@ -575,6 +599,14 @@ const ExcalidrawWrapper = () => {
}, },
files, files,
); );
if (errorMessage) {
setErrorMessage(errorMessage);
}
if (url) {
setLatestShareableLink(url);
}
} catch (error: any) { } catch (error: any) {
if (error.name !== "AbortError") { if (error.name !== "AbortError") {
const { width, height } = canvas; const { width, height } = canvas;
@@ -674,21 +706,47 @@ const ExcalidrawWrapper = () => {
setCollabDialogShown={setCollabDialogShown} setCollabDialogShown={setCollabDialogShown}
isCollabEnabled={!isCollabDisabled} isCollabEnabled={!isCollabDisabled}
/> />
<OverwriteConfirmDialog>
<OverwriteConfirmDialog.Actions.ExportToImage />
<OverwriteConfirmDialog.Actions.SaveToDisk />
{excalidrawAPI && (
<OverwriteConfirmDialog.Action
title={t("overwriteConfirm.action.excalidrawPlus.title")}
actionLabel={t("overwriteConfirm.action.excalidrawPlus.button")}
onClick={() => {
exportToExcalidrawPlus(
excalidrawAPI.getSceneElements(),
excalidrawAPI.getAppState(),
excalidrawAPI.getFiles(),
);
}}
>
{t("overwriteConfirm.action.excalidrawPlus.description")}
</OverwriteConfirmDialog.Action>
)}
</OverwriteConfirmDialog>
<AppFooter /> <AppFooter />
{isCollaborating && isOffline && ( {isCollaborating && isOffline && (
<div className="collab-offline-warning"> <div className="collab-offline-warning">
{t("alerts.collabOfflineWarning")} {t("alerts.collabOfflineWarning")}
</div> </div>
)} )}
{latestShareableLink && (
<ShareableLinkDialog
link={latestShareableLink}
onCloseRequest={() => setLatestShareableLink(null)}
setErrorMessage={setErrorMessage}
/>
)}
{excalidrawAPI && !isCollabDisabled && ( {excalidrawAPI && !isCollabDisabled && (
<Collab excalidrawAPI={excalidrawAPI} /> <Collab excalidrawAPI={excalidrawAPI} />
)} )}
{errorMessage && (
<ErrorDialog onClose={() => setErrorMessage("")}>
{errorMessage}
</ErrorDialog>
)}
</Excalidraw> </Excalidraw>
{errorMessage && (
<ErrorDialog onClose={() => setErrorMessage("")}>
{errorMessage}
</ErrorDialog>
)}
</div> </div>
); );
}; };
+5 -2
View File
@@ -304,7 +304,7 @@ export const groupsAreCompletelyOutOfFrame = (
/** /**
* Returns a map of frameId to frame elements. Includes empty frames. * Returns a map of frameId to frame elements. Includes empty frames.
*/ */
export const groupByFrames = (elements: ExcalidrawElementsIncludingDeleted) => { export const groupByFrames = (elements: readonly ExcalidrawElement[]) => {
const frameElementsMap = new Map< const frameElementsMap = new Map<
ExcalidrawElement["id"], ExcalidrawElement["id"],
ExcalidrawElement[] ExcalidrawElement[]
@@ -591,6 +591,7 @@ export const updateFrameMembershipOfSelectedElements = (
elementsToFilter.forEach((element) => { elementsToFilter.forEach((element) => {
if ( if (
element.frameId &&
!isFrameElement(element) && !isFrameElement(element) &&
!isElementInFrame(element, allElements, appState) !isElementInFrame(element, allElements, appState)
) { ) {
@@ -598,7 +599,9 @@ export const updateFrameMembershipOfSelectedElements = (
} }
}); });
return removeElementsFromFrame(allElements, [...elementsToRemove], appState); return elementsToRemove.size > 0
? removeElementsFromFrame(allElements, [...elementsToRemove], appState)
: allElements;
}; };
/** /**
+1
View File
@@ -10,6 +10,7 @@ export const CODES = {
BRACKET_LEFT: "BracketLeft", BRACKET_LEFT: "BracketLeft",
ONE: "Digit1", ONE: "Digit1",
TWO: "Digit2", TWO: "Digit2",
THREE: "Digit3",
NINE: "Digit9", NINE: "Digit9",
QUOTE: "Quote", QUOTE: "Quote",
ZERO: "Digit0", ZERO: "Digit0",
+5 -1
View File
@@ -124,6 +124,8 @@
}, },
"statusPublished": "نُشر", "statusPublished": "نُشر",
"sidebarLock": "إبقاء الشريط الجانبي مفتوح", "sidebarLock": "إبقاء الشريط الجانبي مفتوح",
"selectAllElementsInFrame": "",
"removeAllElementsFromFrame": "",
"eyeDropper": "" "eyeDropper": ""
}, },
"library": { "library": {
@@ -221,7 +223,9 @@
"penMode": "وضع القلم - امنع اللمس", "penMode": "وضع القلم - امنع اللمس",
"link": "إضافة/تحديث الرابط للشكل المحدد", "link": "إضافة/تحديث الرابط للشكل المحدد",
"eraser": "ممحاة", "eraser": "ممحاة",
"hand": "" "frame": "",
"hand": "",
"extraTools": ""
}, },
"headings": { "headings": {
"canvasActions": "إجراءات اللوحة", "canvasActions": "إجراءات اللوحة",
+5 -1
View File
@@ -124,6 +124,8 @@
}, },
"statusPublished": "", "statusPublished": "",
"sidebarLock": "", "sidebarLock": "",
"selectAllElementsInFrame": "",
"removeAllElementsFromFrame": "",
"eyeDropper": "" "eyeDropper": ""
}, },
"library": { "library": {
@@ -221,7 +223,9 @@
"penMode": "", "penMode": "",
"link": "", "link": "",
"eraser": "", "eraser": "",
"hand": "" "frame": "",
"hand": "",
"extraTools": ""
}, },
"headings": { "headings": {
"canvasActions": "", "canvasActions": "",
+5 -1
View File
@@ -124,6 +124,8 @@
}, },
"statusPublished": "", "statusPublished": "",
"sidebarLock": "", "sidebarLock": "",
"selectAllElementsInFrame": "",
"removeAllElementsFromFrame": "",
"eyeDropper": "" "eyeDropper": ""
}, },
"library": { "library": {
@@ -221,7 +223,9 @@
"penMode": "", "penMode": "",
"link": "", "link": "",
"eraser": "", "eraser": "",
"hand": "" "frame": "",
"hand": "",
"extraTools": ""
}, },
"headings": { "headings": {
"canvasActions": "Действия по платното", "canvasActions": "Действия по платното",
+5 -1
View File
@@ -124,6 +124,8 @@
}, },
"statusPublished": "প্রকাশিত", "statusPublished": "প্রকাশিত",
"sidebarLock": "লক", "sidebarLock": "লক",
"selectAllElementsInFrame": "",
"removeAllElementsFromFrame": "",
"eyeDropper": "" "eyeDropper": ""
}, },
"library": { "library": {
@@ -221,7 +223,9 @@
"penMode": "", "penMode": "",
"link": "একটি নির্বাচিত আকৃতির জন্য লিঙ্ক যোগ বা আপডেট করুন", "link": "একটি নির্বাচিত আকৃতির জন্য লিঙ্ক যোগ বা আপডেট করুন",
"eraser": "ঝাড়ন", "eraser": "ঝাড়ন",
"hand": "" "frame": "",
"hand": "",
"extraTools": ""
}, },
"headings": { "headings": {
"canvasActions": "ক্যানভাস কার্যকলাপ", "canvasActions": "ক্যানভাস কার্যকলাপ",
+5 -1
View File
@@ -124,6 +124,8 @@
}, },
"statusPublished": "Publicat", "statusPublished": "Publicat",
"sidebarLock": "Manté la barra lateral oberta", "sidebarLock": "Manté la barra lateral oberta",
"selectAllElementsInFrame": "",
"removeAllElementsFromFrame": "",
"eyeDropper": "" "eyeDropper": ""
}, },
"library": { "library": {
@@ -221,7 +223,9 @@
"penMode": "Mode de llapis - evita tocar", "penMode": "Mode de llapis - evita tocar",
"link": "Afegeix / actualitza l'enllaç per a la forma seleccionada", "link": "Afegeix / actualitza l'enllaç per a la forma seleccionada",
"eraser": "Esborrador", "eraser": "Esborrador",
"hand": "Mà (eina de desplaçament)" "frame": "",
"hand": "Mà (eina de desplaçament)",
"extraTools": ""
}, },
"headings": { "headings": {
"canvasActions": "Accions del llenç", "canvasActions": "Accions del llenç",
+5 -1
View File
@@ -124,6 +124,8 @@
}, },
"statusPublished": "Zveřejněno", "statusPublished": "Zveřejněno",
"sidebarLock": "Ponechat postranní panel otevřený", "sidebarLock": "Ponechat postranní panel otevřený",
"selectAllElementsInFrame": "",
"removeAllElementsFromFrame": "",
"eyeDropper": "Vyberte barvu z plátna" "eyeDropper": "Vyberte barvu z plátna"
}, },
"library": { "library": {
@@ -221,7 +223,9 @@
"penMode": "Režim Pera - zabránit dotyku", "penMode": "Režim Pera - zabránit dotyku",
"link": "Přidat/aktualizovat odkaz pro vybraný tvar", "link": "Přidat/aktualizovat odkaz pro vybraný tvar",
"eraser": "Guma", "eraser": "Guma",
"hand": "Ruka (nástroj pro posouvání)" "frame": "",
"hand": "Ruka (nástroj pro posouvání)",
"extraTools": ""
}, },
"headings": { "headings": {
"canvasActions": "Akce plátna", "canvasActions": "Akce plátna",
+5 -1
View File
@@ -124,6 +124,8 @@
}, },
"statusPublished": "", "statusPublished": "",
"sidebarLock": "", "sidebarLock": "",
"selectAllElementsInFrame": "",
"removeAllElementsFromFrame": "",
"eyeDropper": "" "eyeDropper": ""
}, },
"library": { "library": {
@@ -221,7 +223,9 @@
"penMode": "", "penMode": "",
"link": "", "link": "",
"eraser": "", "eraser": "",
"hand": "" "frame": "",
"hand": "",
"extraTools": ""
}, },
"headings": { "headings": {
"canvasActions": "", "canvasActions": "",
+5 -1
View File
@@ -124,6 +124,8 @@
}, },
"statusPublished": "Veröffentlicht", "statusPublished": "Veröffentlicht",
"sidebarLock": "Seitenleiste offen lassen", "sidebarLock": "Seitenleiste offen lassen",
"selectAllElementsInFrame": "Alle Elemente im Rahmen auswählen",
"removeAllElementsFromFrame": "Alle Elemente aus dem Rahmen entfernen",
"eyeDropper": "Farbe von der Zeichenfläche auswählen" "eyeDropper": "Farbe von der Zeichenfläche auswählen"
}, },
"library": { "library": {
@@ -221,7 +223,9 @@
"penMode": "Stift-Modus - Berührung verhindern", "penMode": "Stift-Modus - Berührung verhindern",
"link": "Link für ausgewählte Form hinzufügen / aktualisieren", "link": "Link für ausgewählte Form hinzufügen / aktualisieren",
"eraser": "Radierer", "eraser": "Radierer",
"hand": "Hand (Schwenkwerkzeug)" "frame": "Rahmenwerkzeug",
"hand": "Hand (Schwenkwerkzeug)",
"extraTools": "Weitere Werkzeuge"
}, },
"headings": { "headings": {
"canvasActions": "Aktionen für Zeichenfläche", "canvasActions": "Aktionen für Zeichenfläche",
+5 -1
View File
@@ -124,6 +124,8 @@
}, },
"statusPublished": "Δημοσιευμένο", "statusPublished": "Δημοσιευμένο",
"sidebarLock": "Κρατήστε την πλαϊνή μπάρα ανοιχτή", "sidebarLock": "Κρατήστε την πλαϊνή μπάρα ανοιχτή",
"selectAllElementsInFrame": "",
"removeAllElementsFromFrame": "",
"eyeDropper": "" "eyeDropper": ""
}, },
"library": { "library": {
@@ -221,7 +223,9 @@
"penMode": "Λειτουργία μολυβιού - αποτροπή αφής", "penMode": "Λειτουργία μολυβιού - αποτροπή αφής",
"link": "Προσθήκη/ Ενημέρωση συνδέσμου για ένα επιλεγμένο σχήμα", "link": "Προσθήκη/ Ενημέρωση συνδέσμου για ένα επιλεγμένο σχήμα",
"eraser": "Γόμα", "eraser": "Γόμα",
"hand": "" "frame": "",
"hand": "",
"extraTools": ""
}, },
"headings": { "headings": {
"canvasActions": "Ενέργειες καμβά", "canvasActions": "Ενέργειες καμβά",
+31
View File
@@ -449,5 +449,36 @@
"shades": "Shades", "shades": "Shades",
"hexCode": "Hex code", "hexCode": "Hex code",
"noShades": "No shades available for this color" "noShades": "No shades available for this color"
},
"overwriteConfirm": {
"action": {
"exportToImage": {
"title": "Export as image",
"button": "Export as image",
"description": "Export the scene data as an image from which you can import later."
},
"saveToDisk": {
"title": "Save to disk",
"button": "Save to disk",
"description": "Export the scene data to a file from which you can import later."
},
"excalidrawPlus": {
"title": "Excalidraw+",
"button": "Export to Excalidraw+",
"description": "Save the scene to your Excalidraw+ workspace."
}
},
"modal": {
"loadFromFile": {
"title": "Load from file",
"button": "Load from file",
"description": "Loading from a file will <bold>replace your existing content</bold>.<br></br>You can back up your drawing first using one of the options below."
},
"shareableLink": {
"title": "Load from link",
"button": "Replace my content",
"description": "Loading external drawing will <bold>replace your existing content</bold>.<br></br>You can back up your drawing first by using one of the options below."
}
}
} }
} }
+5 -1
View File
@@ -124,6 +124,8 @@
}, },
"statusPublished": "Publicado", "statusPublished": "Publicado",
"sidebarLock": "Mantener barra lateral abierta", "sidebarLock": "Mantener barra lateral abierta",
"selectAllElementsInFrame": "",
"removeAllElementsFromFrame": "",
"eyeDropper": "" "eyeDropper": ""
}, },
"library": { "library": {
@@ -221,7 +223,9 @@
"penMode": "Modo Lápiz - previene toque", "penMode": "Modo Lápiz - previene toque",
"link": "Añadir/Actualizar enlace para una forma seleccionada", "link": "Añadir/Actualizar enlace para una forma seleccionada",
"eraser": "Borrar", "eraser": "Borrar",
"hand": "Mano (herramienta de panoramización)" "frame": "",
"hand": "Mano (herramienta de panoramización)",
"extraTools": ""
}, },
"headings": { "headings": {
"canvasActions": "Acciones del lienzo", "canvasActions": "Acciones del lienzo",
+39 -35
View File
@@ -124,7 +124,9 @@
}, },
"statusPublished": "Argitaratua", "statusPublished": "Argitaratua",
"sidebarLock": "Mantendu alboko barra irekita", "sidebarLock": "Mantendu alboko barra irekita",
"eyeDropper": "" "selectAllElementsInFrame": "Hautatu markoko elementu guztiak",
"removeAllElementsFromFrame": "Kendu markoko elementu guztiak",
"eyeDropper": "Aukeratu kolorea oihaletik"
}, },
"library": { "library": {
"noItems": "Oraindik ez da elementurik gehitu...", "noItems": "Oraindik ez da elementurik gehitu...",
@@ -221,7 +223,9 @@
"penMode": "Luma modua - ukipena saihestu", "penMode": "Luma modua - ukipena saihestu",
"link": "Gehitu / Eguneratu esteka hautatutako forma baterako", "link": "Gehitu / Eguneratu esteka hautatutako forma baterako",
"eraser": "Borragoma", "eraser": "Borragoma",
"hand": "Eskua (panoratze tresna)" "frame": "Marko tresna",
"hand": "Eskua (panoratze tresna)",
"extraTools": "Tresna gehiago"
}, },
"headings": { "headings": {
"canvasActions": "Canvas ekintzak", "canvasActions": "Canvas ekintzak",
@@ -356,27 +360,27 @@
"removeItemsFromLib": "Kendu hautatutako elementuak liburutegitik" "removeItemsFromLib": "Kendu hautatutako elementuak liburutegitik"
}, },
"imageExportDialog": { "imageExportDialog": {
"header": "", "header": "Esportatu irudia",
"label": { "label": {
"withBackground": "", "withBackground": "Atzeko planoa",
"onlySelected": "", "onlySelected": "Hautapena soilik",
"darkMode": "", "darkMode": "Modu iluna",
"embedScene": "", "embedScene": "Txertatu eszena",
"scale": "", "scale": "Eskala",
"padding": "" "padding": "Betegarria"
}, },
"tooltip": { "tooltip": {
"embedScene": "" "embedScene": "Eszenaren datuak esportatutako PNG/SVG fitxategian gordeko dira, eszena bertatik berrezartzeko.\nEsportatutako fitxategien tamaina handituko da."
}, },
"title": { "title": {
"exportToPng": "", "exportToPng": "Esportatu PNG gisa",
"exportToSvg": "", "exportToSvg": "Esportatu SVG gisa",
"copyPngToClipboard": "" "copyPngToClipboard": "Kopiatu PNG arbelera"
}, },
"button": { "button": {
"exportToPng": "", "exportToPng": "PNG",
"exportToSvg": "", "exportToSvg": "SVG",
"copyPngToClipboard": "" "copyPngToClipboard": "Kopiatu arbelean"
} }
}, },
"encrypted": { "encrypted": {
@@ -411,20 +415,20 @@
}, },
"colors": { "colors": {
"transparent": "Gardena", "transparent": "Gardena",
"black": "", "black": "Beltza",
"white": "", "white": "Zuria",
"red": "", "red": "Gorria",
"pink": "", "pink": "Arrosa",
"grape": "", "grape": "Mahats kolorea",
"violet": "", "violet": "Bioleta",
"gray": "", "gray": "Grisa",
"blue": "", "blue": "Urdina",
"cyan": "", "cyan": "Ziana",
"teal": "", "teal": "Berde urdinxka",
"green": "", "green": "Berdea",
"yellow": "", "yellow": "Horia",
"orange": "", "orange": "Laranja",
"bronze": "" "bronze": "Brontzea"
}, },
"welcomeScreen": { "welcomeScreen": {
"app": { "app": {
@@ -440,10 +444,10 @@
} }
}, },
"colorPicker": { "colorPicker": {
"mostUsedCustomColors": "", "mostUsedCustomColors": "Gehien erabilitako kolore pertsonalizatuak",
"colors": "", "colors": "Koloreak",
"shades": "", "shades": "Ñabardurak",
"hexCode": "", "hexCode": "Hez kodea",
"noShades": "" "noShades": "Kolore honetarako ez dago ñabardurarik eskuragarri"
} }
} }
+5 -1
View File
@@ -124,6 +124,8 @@
}, },
"statusPublished": "منتشر شده", "statusPublished": "منتشر شده",
"sidebarLock": "باز نگه داشتن سایدبار", "sidebarLock": "باز نگه داشتن سایدبار",
"selectAllElementsInFrame": "",
"removeAllElementsFromFrame": "",
"eyeDropper": "" "eyeDropper": ""
}, },
"library": { "library": {
@@ -221,7 +223,9 @@
"penMode": "حالت قلم - جلوگیری از تماس", "penMode": "حالت قلم - جلوگیری از تماس",
"link": "افزودن/به‌روزرسانی پیوند برای شکل انتخابی", "link": "افزودن/به‌روزرسانی پیوند برای شکل انتخابی",
"eraser": "پاک کن", "eraser": "پاک کن",
"hand": "دست (ابزار پانینگ)" "frame": "",
"hand": "دست (ابزار پانینگ)",
"extraTools": ""
}, },
"headings": { "headings": {
"canvasActions": "عملیات روی بوم", "canvasActions": "عملیات روی بوم",
+5 -1
View File
@@ -124,6 +124,8 @@
}, },
"statusPublished": "Julkaistu", "statusPublished": "Julkaistu",
"sidebarLock": "Pidä sivupalkki avoinna", "sidebarLock": "Pidä sivupalkki avoinna",
"selectAllElementsInFrame": "",
"removeAllElementsFromFrame": "",
"eyeDropper": "" "eyeDropper": ""
}, },
"library": { "library": {
@@ -221,7 +223,9 @@
"penMode": "Kynätila - estä kosketus", "penMode": "Kynätila - estä kosketus",
"link": "Lisää/päivitä linkki valitulle muodolle", "link": "Lisää/päivitä linkki valitulle muodolle",
"eraser": "Poistotyökalu", "eraser": "Poistotyökalu",
"hand": "Käsi (panning-työkalu)" "frame": "",
"hand": "Käsi (panning-työkalu)",
"extraTools": ""
}, },
"headings": { "headings": {
"canvasActions": "Piirtoalueen toiminnot", "canvasActions": "Piirtoalueen toiminnot",
+5 -1
View File
@@ -124,6 +124,8 @@
}, },
"statusPublished": "Publié", "statusPublished": "Publié",
"sidebarLock": "Maintenir la barre latérale ouverte", "sidebarLock": "Maintenir la barre latérale ouverte",
"selectAllElementsInFrame": "",
"removeAllElementsFromFrame": "",
"eyeDropper": "" "eyeDropper": ""
}, },
"library": { "library": {
@@ -221,7 +223,9 @@
"penMode": "Mode stylo - évite le toucher", "penMode": "Mode stylo - évite le toucher",
"link": "Ajouter/mettre à jour le lien pour une forme sélectionnée", "link": "Ajouter/mettre à jour le lien pour une forme sélectionnée",
"eraser": "Gomme", "eraser": "Gomme",
"hand": "Mains (outil de déplacement de la vue)" "frame": "",
"hand": "Mains (outil de déplacement de la vue)",
"extraTools": ""
}, },
"headings": { "headings": {
"canvasActions": "Actions du canevas", "canvasActions": "Actions du canevas",
+5 -1
View File
@@ -124,6 +124,8 @@
}, },
"statusPublished": "Publicado", "statusPublished": "Publicado",
"sidebarLock": "Manter a barra lateral aberta", "sidebarLock": "Manter a barra lateral aberta",
"selectAllElementsInFrame": "",
"removeAllElementsFromFrame": "",
"eyeDropper": "" "eyeDropper": ""
}, },
"library": { "library": {
@@ -221,7 +223,9 @@
"penMode": "Modo lapis - evitar o contacto", "penMode": "Modo lapis - evitar o contacto",
"link": "Engadir/ Actualizar ligazón para a forma seleccionada", "link": "Engadir/ Actualizar ligazón para a forma seleccionada",
"eraser": "Goma de borrar", "eraser": "Goma de borrar",
"hand": "Man (ferramenta de desprazamento)" "frame": "",
"hand": "Man (ferramenta de desprazamento)",
"extraTools": ""
}, },
"headings": { "headings": {
"canvasActions": "Accións do lenzo", "canvasActions": "Accións do lenzo",
+5 -1
View File
@@ -124,6 +124,8 @@
}, },
"statusPublished": "פורסם", "statusPublished": "פורסם",
"sidebarLock": "שמור את סרגל הצד פתוח", "sidebarLock": "שמור את סרגל הצד פתוח",
"selectAllElementsInFrame": "",
"removeAllElementsFromFrame": "",
"eyeDropper": "" "eyeDropper": ""
}, },
"library": { "library": {
@@ -221,7 +223,9 @@
"penMode": "מצב עט - מנע נגיעה", "penMode": "מצב עט - מנע נגיעה",
"link": "הוספה/עדכון קישור של הצורה שנבחרה", "link": "הוספה/עדכון קישור של הצורה שנבחרה",
"eraser": "מחק", "eraser": "מחק",
"hand": "יד (כלי הזזה)" "frame": "",
"hand": "יד (כלי הזזה)",
"extraTools": ""
}, },
"headings": { "headings": {
"canvasActions": "פעולות קנבאס", "canvasActions": "פעולות קנבאס",
+5 -1
View File
@@ -124,6 +124,8 @@
}, },
"statusPublished": "प्रकाशित", "statusPublished": "प्रकाशित",
"sidebarLock": "साइडबार खुला रखे.", "sidebarLock": "साइडबार खुला रखे.",
"selectAllElementsInFrame": "",
"removeAllElementsFromFrame": "",
"eyeDropper": "चित्रफलक से रंग चुने" "eyeDropper": "चित्रफलक से रंग चुने"
}, },
"library": { "library": {
@@ -221,7 +223,9 @@
"penMode": "पेन का मोड - स्पर्श टाले", "penMode": "पेन का मोड - स्पर्श टाले",
"link": "", "link": "",
"eraser": "रबड़", "eraser": "रबड़",
"hand": "हाथ ( खिसकाने का औज़ार)" "frame": "",
"hand": "हाथ ( खिसकाने का औज़ार)",
"extraTools": ""
}, },
"headings": { "headings": {
"canvasActions": "कैनवास क्रिया", "canvasActions": "कैनवास क्रिया",
+5 -1
View File
@@ -124,6 +124,8 @@
}, },
"statusPublished": "", "statusPublished": "",
"sidebarLock": "", "sidebarLock": "",
"selectAllElementsInFrame": "",
"removeAllElementsFromFrame": "",
"eyeDropper": "" "eyeDropper": ""
}, },
"library": { "library": {
@@ -221,7 +223,9 @@
"penMode": "", "penMode": "",
"link": "Hivatkozás hozzáadása/frissítése a kiválasztott alakzathoz", "link": "Hivatkozás hozzáadása/frissítése a kiválasztott alakzathoz",
"eraser": "", "eraser": "",
"hand": "" "frame": "",
"hand": "",
"extraTools": ""
}, },
"headings": { "headings": {
"canvasActions": "Vászon műveletek", "canvasActions": "Vászon műveletek",
+5 -1
View File
@@ -124,6 +124,8 @@
}, },
"statusPublished": "Telah terbit", "statusPublished": "Telah terbit",
"sidebarLock": "Biarkan sidebar tetap terbuka", "sidebarLock": "Biarkan sidebar tetap terbuka",
"selectAllElementsInFrame": "",
"removeAllElementsFromFrame": "",
"eyeDropper": "" "eyeDropper": ""
}, },
"library": { "library": {
@@ -221,7 +223,9 @@
"penMode": "Mode pena - mencegah sentuhan", "penMode": "Mode pena - mencegah sentuhan",
"link": "Tambah/Perbarui tautan untuk bentuk yang dipilih", "link": "Tambah/Perbarui tautan untuk bentuk yang dipilih",
"eraser": "Penghapus", "eraser": "Penghapus",
"hand": "Tangan (alat panning)" "frame": "",
"hand": "Tangan (alat panning)",
"extraTools": ""
}, },
"headings": { "headings": {
"canvasActions": "Opsi Kanvas", "canvasActions": "Opsi Kanvas",
+7 -3
View File
@@ -124,6 +124,8 @@
}, },
"statusPublished": "Pubblicato", "statusPublished": "Pubblicato",
"sidebarLock": "Mantieni aperta la barra laterale", "sidebarLock": "Mantieni aperta la barra laterale",
"selectAllElementsInFrame": "",
"removeAllElementsFromFrame": "",
"eyeDropper": "" "eyeDropper": ""
}, },
"library": { "library": {
@@ -221,7 +223,9 @@
"penMode": "Modalità penna - previene il tocco", "penMode": "Modalità penna - previene il tocco",
"link": "Aggiungi/ aggiorna il link per una forma selezionata", "link": "Aggiungi/ aggiorna il link per una forma selezionata",
"eraser": "Gomma", "eraser": "Gomma",
"hand": "Mano (strumento di panoramica)" "frame": "",
"hand": "Mano (strumento di panoramica)",
"extraTools": "Altri strumenti"
}, },
"headings": { "headings": {
"canvasActions": "Azioni sulla Tela", "canvasActions": "Azioni sulla Tela",
@@ -361,12 +365,12 @@
"withBackground": "Sfondo", "withBackground": "Sfondo",
"onlySelected": "Solo selezionato", "onlySelected": "Solo selezionato",
"darkMode": "Tema scuro", "darkMode": "Tema scuro",
"embedScene": "", "embedScene": "Includi scena",
"scale": "Scala", "scale": "Scala",
"padding": "" "padding": ""
}, },
"tooltip": { "tooltip": {
"embedScene": "" "embedScene": "I dati della scena saranno salvati nel file PNG/SVG esportato in modo che la scena possa essere ripristinata da esso.\nQuesto aumenterà la dimensione del file esportato."
}, },
"title": { "title": {
"exportToPng": "Esporta come PNG", "exportToPng": "Esporta come PNG",
+5 -1
View File
@@ -124,6 +124,8 @@
}, },
"statusPublished": "公開済み", "statusPublished": "公開済み",
"sidebarLock": "サイドバーを開いたままにする", "sidebarLock": "サイドバーを開いたままにする",
"selectAllElementsInFrame": "",
"removeAllElementsFromFrame": "",
"eyeDropper": "" "eyeDropper": ""
}, },
"library": { "library": {
@@ -221,7 +223,9 @@
"penMode": "ペンモード - タッチ防止", "penMode": "ペンモード - タッチ防止",
"link": "選択した図形のリンクを追加/更新", "link": "選択した図形のリンクを追加/更新",
"eraser": "消しゴム", "eraser": "消しゴム",
"hand": "手 (パンニングツール)" "frame": "",
"hand": "手 (パンニングツール)",
"extraTools": ""
}, },
"headings": { "headings": {
"canvasActions": "キャンバス操作", "canvasActions": "キャンバス操作",
+5 -1
View File
@@ -124,6 +124,8 @@
}, },
"statusPublished": "", "statusPublished": "",
"sidebarLock": "", "sidebarLock": "",
"selectAllElementsInFrame": "",
"removeAllElementsFromFrame": "",
"eyeDropper": "" "eyeDropper": ""
}, },
"library": { "library": {
@@ -221,7 +223,9 @@
"penMode": "", "penMode": "",
"link": "", "link": "",
"eraser": "Óshirgish", "eraser": "Óshirgish",
"hand": "" "frame": "",
"hand": "",
"extraTools": ""
}, },
"headings": { "headings": {
"canvasActions": "", "canvasActions": "",
+5 -1
View File
@@ -124,6 +124,8 @@
}, },
"statusPublished": "Yeffeɣ-d", "statusPublished": "Yeffeɣ-d",
"sidebarLock": "Eǧǧ afeggag n yidis yeldi", "sidebarLock": "Eǧǧ afeggag n yidis yeldi",
"selectAllElementsInFrame": "",
"removeAllElementsFromFrame": "",
"eyeDropper": "" "eyeDropper": ""
}, },
"library": { "library": {
@@ -221,7 +223,9 @@
"penMode": "Askar n yimru - gdel tanalit", "penMode": "Askar n yimru - gdel tanalit",
"link": "Rnu/leqqem aseɣwen i talɣa yettwafernen", "link": "Rnu/leqqem aseɣwen i talɣa yettwafernen",
"eraser": "Sfeḍ", "eraser": "Sfeḍ",
"hand": "Afus (afecku n usmutti n tmuɣli)" "frame": "",
"hand": "Afus (afecku n usmutti n tmuɣli)",
"extraTools": ""
}, },
"headings": { "headings": {
"canvasActions": "Tigawin n teɣzut n usuneɣ", "canvasActions": "Tigawin n teɣzut n usuneɣ",
+5 -1
View File
@@ -124,6 +124,8 @@
}, },
"statusPublished": "", "statusPublished": "",
"sidebarLock": "", "sidebarLock": "",
"selectAllElementsInFrame": "",
"removeAllElementsFromFrame": "",
"eyeDropper": "" "eyeDropper": ""
}, },
"library": { "library": {
@@ -221,7 +223,9 @@
"penMode": "", "penMode": "",
"link": "", "link": "",
"eraser": "", "eraser": "",
"hand": "" "frame": "",
"hand": "",
"extraTools": ""
}, },
"headings": { "headings": {
"canvasActions": "", "canvasActions": "",
+5 -1
View File
@@ -124,6 +124,8 @@
}, },
"statusPublished": "ត្រូវបានបោះពុម្ពផ្សាយ", "statusPublished": "ត្រូវបានបោះពុម្ពផ្សាយ",
"sidebarLock": "ទុករបារចំហៀងបើក", "sidebarLock": "ទុករបារចំហៀងបើក",
"selectAllElementsInFrame": "",
"removeAllElementsFromFrame": "",
"eyeDropper": "" "eyeDropper": ""
}, },
"library": { "library": {
@@ -221,7 +223,9 @@
"penMode": "របៀបប៊ិច - ជៀសវាងការប៉ះ", "penMode": "របៀបប៊ិច - ជៀសវាងការប៉ះ",
"link": "បន្ថែម/ធ្វើបច្ចុប្បន្នភាពតំណភ្ជាប់សម្រាប់រូបរាងដែលបានជ្រើសរើស", "link": "បន្ថែម/ធ្វើបច្ចុប្បន្នភាពតំណភ្ជាប់សម្រាប់រូបរាងដែលបានជ្រើសរើស",
"eraser": "ជ័រលុប", "eraser": "ជ័រលុប",
"hand": "ដៃ (panning tool)" "frame": "",
"hand": "ដៃ (panning tool)",
"extraTools": ""
}, },
"headings": { "headings": {
"canvasActions": "សកម្មភាពបាវ", "canvasActions": "សកម្មភាពបាវ",
+5 -1
View File
@@ -124,6 +124,8 @@
}, },
"statusPublished": "게시됨", "statusPublished": "게시됨",
"sidebarLock": "사이드바 유지", "sidebarLock": "사이드바 유지",
"selectAllElementsInFrame": "프레임의 모든 요소 선택",
"removeAllElementsFromFrame": "프레임의 모든 요소 삭제",
"eyeDropper": "캔버스에서 색상 고르기" "eyeDropper": "캔버스에서 색상 고르기"
}, },
"library": { "library": {
@@ -221,7 +223,9 @@
"penMode": "펜 모드 - 터치 방지", "penMode": "펜 모드 - 터치 방지",
"link": "선택한 도형에 대해서 링크를 추가/업데이트", "link": "선택한 도형에 대해서 링크를 추가/업데이트",
"eraser": "지우개", "eraser": "지우개",
"hand": "손 (패닝 도구)" "frame": "프레임 도구",
"hand": "손 (패닝 도구)",
"extraTools": "다른 도구"
}, },
"headings": { "headings": {
"canvasActions": "캔버스 동작", "canvasActions": "캔버스 동작",
+40 -36
View File
@@ -71,7 +71,7 @@
"language": "زمان", "language": "زمان",
"liveCollaboration": "هاوکاریکردنی زیندو...", "liveCollaboration": "هاوکاریکردنی زیندو...",
"duplicateSelection": "لەبەرگرتنەوە", "duplicateSelection": "لەبەرگرتنەوە",
"untitled": "بێ-ناو", "untitled": "بێ ناونیشان",
"name": "ناو", "name": "ناو",
"yourName": "ناوەکەت", "yourName": "ناوەکەت",
"madeWithExcalidraw": "دروستکراوە بە Excalidraw", "madeWithExcalidraw": "دروستکراوە بە Excalidraw",
@@ -124,7 +124,9 @@
}, },
"statusPublished": "بڵاوکراوەتەوە", "statusPublished": "بڵاوکراوەتەوە",
"sidebarLock": "هێشتنەوەی شریتی لا بە کراوەیی", "sidebarLock": "هێشتنەوەی شریتی لا بە کراوەیی",
"eyeDropper": "" "selectAllElementsInFrame": "هەموو توخمەکانی ناو چوارچێوەکە دیاری بکە",
"removeAllElementsFromFrame": "هەموو توخمەکانی ناو چوارچێوەکە لابەرە",
"eyeDropper": "ڕەنگێک لەسەر تابلۆکە هەڵبژێرە"
}, },
"library": { "library": {
"noItems": "هێشتا هیچ بڕگەیەک زیاد نەکراوە...", "noItems": "هێشتا هیچ بڕگەیەک زیاد نەکراوە...",
@@ -221,7 +223,9 @@
"penMode": "شێوازی قەڵەم - دەست لێدان ڕابگرە", "penMode": "شێوازی قەڵەم - دەست لێدان ڕابگرە",
"link": "زیادکردن/ نوێکردنەوەی لینک بۆ شێوەی دیاریکراو", "link": "زیادکردن/ نوێکردنەوەی لینک بۆ شێوەی دیاریکراو",
"eraser": "سڕەر", "eraser": "سڕەر",
"hand": "دەست (ئامرازی پانکردن)" "frame": "ئامرازی چوارچێوە",
"hand": "دەست (ئامرازی پانکردن)",
"extraTools": "ئامرازی زیاتر"
}, },
"headings": { "headings": {
"canvasActions": "کردارەکانی تابلۆ", "canvasActions": "کردارەکانی تابلۆ",
@@ -356,27 +360,27 @@
"removeItemsFromLib": "لابردنی ئایتمە دیاریکراوەکان لە کتێبخانە" "removeItemsFromLib": "لابردنی ئایتمە دیاریکراوەکان لە کتێبخانە"
}, },
"imageExportDialog": { "imageExportDialog": {
"header": "", "header": "وێنە هەناردە بکە",
"label": { "label": {
"withBackground": "", "withBackground": "پاشبنەما",
"onlySelected": "", "onlySelected": "تەنها دیاریکراوەکان",
"darkMode": "", "darkMode": "دۆخی تاریک",
"embedScene": "", "embedScene": "دیمەنەکە بەکاربهێنەرەوە",
"scale": "", "scale": "قەبارە",
"padding": "" "padding": "بۆشایی"
}, },
"tooltip": { "tooltip": {
"embedScene": "" "embedScene": "داتاکانی دیمەنەکە لە فایلە هەناردەکراوەکەی PNG/SVG هەڵدەگیرێن بۆ ئەوەی دیمەنەکە لێیەوە بگەڕێتەوە.\nقەبارەی پەڕگەی هەناردەکراو زیاد دەکات."
}, },
"title": { "title": {
"exportToPng": "", "exportToPng": "هەناردە بکە وەک PNG",
"exportToSvg": "", "exportToSvg": "هەناردە بکە وەک SVG",
"copyPngToClipboard": "" "copyPngToClipboard": "لەبەربگرەوە بۆ سەر تەختەنوس"
}, },
"button": { "button": {
"exportToPng": "", "exportToPng": "PNG",
"exportToSvg": "", "exportToSvg": "SVG",
"copyPngToClipboard": "" "copyPngToClipboard": "له‌به‌ری بگره‌وه‌ بۆ ته‌خته‌نووس"
} }
}, },
"encrypted": { "encrypted": {
@@ -411,20 +415,20 @@
}, },
"colors": { "colors": {
"transparent": "ڕوون", "transparent": "ڕوون",
"black": "", "black": "ڕەش",
"white": "", "white": "سپی",
"red": "", "red": "سور",
"pink": "", "pink": "پەمەیی",
"grape": "", "grape": "مێوژی",
"violet": "", "violet": "مۆری کاڵ",
"gray": "", "gray": "خۆڵەمێشی",
"blue": "", "blue": "شین",
"cyan": "", "cyan": "شینی ئاسمانی",
"teal": "", "teal": "شەدری",
"green": "", "green": "سه‌وز",
"yellow": "", "yellow": "زەرد",
"orange": "", "orange": "پرتەقاڵی",
"bronze": "" "bronze": "برۆنزی"
}, },
"welcomeScreen": { "welcomeScreen": {
"app": { "app": {
@@ -440,10 +444,10 @@
} }
}, },
"colorPicker": { "colorPicker": {
"mostUsedCustomColors": "", "mostUsedCustomColors": "زۆرترین ڕەنگە باوە بەکارهاتووەکان",
"colors": "", "colors": "ڕەنگەکان",
"shades": "", "shades": "سێبەرەکان",
"hexCode": "", "hexCode": "کۆدی هێکس",
"noShades": "" "noShades": "هیچ سێبەرێک بۆ ئەم ڕەنگە بەردەست نییە"
} }
} }
+5 -1
View File
@@ -124,6 +124,8 @@
}, },
"statusPublished": "", "statusPublished": "",
"sidebarLock": "", "sidebarLock": "",
"selectAllElementsInFrame": "",
"removeAllElementsFromFrame": "",
"eyeDropper": "" "eyeDropper": ""
}, },
"library": { "library": {
@@ -221,7 +223,9 @@
"penMode": "Rašyklio režimas - neleisti prisilietimų", "penMode": "Rašyklio režimas - neleisti prisilietimų",
"link": "Pridėti / Atnaujinti pasirinktos figūros nuorodą", "link": "Pridėti / Atnaujinti pasirinktos figūros nuorodą",
"eraser": "Trintukas", "eraser": "Trintukas",
"hand": "" "frame": "",
"hand": "",
"extraTools": ""
}, },
"headings": { "headings": {
"canvasActions": "Veiksmai su drobe", "canvasActions": "Veiksmai su drobe",
+5 -1
View File
@@ -124,6 +124,8 @@
}, },
"statusPublished": "Publicēts", "statusPublished": "Publicēts",
"sidebarLock": "Paturēt atvērtu sānjoslu", "sidebarLock": "Paturēt atvērtu sānjoslu",
"selectAllElementsInFrame": "",
"removeAllElementsFromFrame": "",
"eyeDropper": "" "eyeDropper": ""
}, },
"library": { "library": {
@@ -221,7 +223,9 @@
"penMode": "Pildspalvas režīms novērst pieskaršanos", "penMode": "Pildspalvas režīms novērst pieskaršanos",
"link": "Pievienot/rediģēt atlasītās figūras saiti", "link": "Pievienot/rediģēt atlasītās figūras saiti",
"eraser": "Dzēšgumija", "eraser": "Dzēšgumija",
"hand": "Roka (panoramēšanas rīks)" "frame": "",
"hand": "Roka (panoramēšanas rīks)",
"extraTools": ""
}, },
"headings": { "headings": {
"canvasActions": "Tāfeles darbības", "canvasActions": "Tāfeles darbības",
+5 -1
View File
@@ -124,6 +124,8 @@
}, },
"statusPublished": "प्रकाशित करा", "statusPublished": "प्रकाशित करा",
"sidebarLock": "साइडबार उघडं ठेवा", "sidebarLock": "साइडबार उघडं ठेवा",
"selectAllElementsInFrame": "",
"removeAllElementsFromFrame": "",
"eyeDropper": "चित्रफलकातून रंग निवडा" "eyeDropper": "चित्रफलकातून रंग निवडा"
}, },
"library": { "library": {
@@ -221,7 +223,9 @@
"penMode": "पेन चा मोड - स्पर्श टाळा", "penMode": "पेन चा मोड - स्पर्श टाळा",
"link": "निवडलेल्या आकारासाठी दुवा जोडा/बदल करा", "link": "निवडलेल्या आकारासाठी दुवा जोडा/बदल करा",
"eraser": "खोड रबर", "eraser": "खोड रबर",
"hand": "हात ( सरकवण्या चे उपकरण)" "frame": "",
"hand": "हात ( सरकवण्या चे उपकरण)",
"extraTools": ""
}, },
"headings": { "headings": {
"canvasActions": "पटल क्रिया", "canvasActions": "पटल क्रिया",
+5 -1
View File
@@ -124,6 +124,8 @@
}, },
"statusPublished": "", "statusPublished": "",
"sidebarLock": "", "sidebarLock": "",
"selectAllElementsInFrame": "",
"removeAllElementsFromFrame": "",
"eyeDropper": "" "eyeDropper": ""
}, },
"library": { "library": {
@@ -221,7 +223,9 @@
"penMode": "", "penMode": "",
"link": "", "link": "",
"eraser": "", "eraser": "",
"hand": "" "frame": "",
"hand": "",
"extraTools": ""
}, },
"headings": { "headings": {
"canvasActions": "ကားချပ်လုပ်ဆောင်ချက်", "canvasActions": "ကားချပ်လုပ်ဆောင်ချက်",
+5 -1
View File
@@ -124,6 +124,8 @@
}, },
"statusPublished": "Publisert", "statusPublished": "Publisert",
"sidebarLock": "Holde sidemenyen åpen", "sidebarLock": "Holde sidemenyen åpen",
"selectAllElementsInFrame": "Velg alle elementene i rammen",
"removeAllElementsFromFrame": "Fjern alle elementer fra rammen",
"eyeDropper": "Velg farge fra lerretet" "eyeDropper": "Velg farge fra lerretet"
}, },
"library": { "library": {
@@ -221,7 +223,9 @@
"penMode": "Pennemodus - forhindre berøring", "penMode": "Pennemodus - forhindre berøring",
"link": "Legg til / oppdater link for en valgt figur", "link": "Legg til / oppdater link for en valgt figur",
"eraser": "Viskelær", "eraser": "Viskelær",
"hand": "Hånd (panoreringsverktøy)" "frame": "Rammeverktøy",
"hand": "Hånd (panoreringsverktøy)",
"extraTools": "Flere verktøy"
}, },
"headings": { "headings": {
"canvasActions": "Handlinger: lerret", "canvasActions": "Handlinger: lerret",
+5 -1
View File
@@ -124,6 +124,8 @@
}, },
"statusPublished": "Gepubliceerd", "statusPublished": "Gepubliceerd",
"sidebarLock": "Zijbalk open houden", "sidebarLock": "Zijbalk open houden",
"selectAllElementsInFrame": "",
"removeAllElementsFromFrame": "",
"eyeDropper": "" "eyeDropper": ""
}, },
"library": { "library": {
@@ -221,7 +223,9 @@
"penMode": "Pen modus - Blokkeer aanraken", "penMode": "Pen modus - Blokkeer aanraken",
"link": "Link toevoegen / bijwerken voor een geselecteerde vorm", "link": "Link toevoegen / bijwerken voor een geselecteerde vorm",
"eraser": "Gum", "eraser": "Gum",
"hand": "" "frame": "",
"hand": "",
"extraTools": ""
}, },
"headings": { "headings": {
"canvasActions": "Canvasacties", "canvasActions": "Canvasacties",
+5 -1
View File
@@ -124,6 +124,8 @@
}, },
"statusPublished": "", "statusPublished": "",
"sidebarLock": "", "sidebarLock": "",
"selectAllElementsInFrame": "",
"removeAllElementsFromFrame": "",
"eyeDropper": "" "eyeDropper": ""
}, },
"library": { "library": {
@@ -221,7 +223,9 @@
"penMode": "", "penMode": "",
"link": "Legg til/ oppdater lenke til valt figur", "link": "Legg til/ oppdater lenke til valt figur",
"eraser": "Viskelêr", "eraser": "Viskelêr",
"hand": "" "frame": "",
"hand": "",
"extraTools": ""
}, },
"headings": { "headings": {
"canvasActions": "Handlingar: lerret", "canvasActions": "Handlingar: lerret",
+5 -1
View File
@@ -124,6 +124,8 @@
}, },
"statusPublished": "Publicat", "statusPublished": "Publicat",
"sidebarLock": "Gardar la barra laterala dobèrta", "sidebarLock": "Gardar la barra laterala dobèrta",
"selectAllElementsInFrame": "",
"removeAllElementsFromFrame": "",
"eyeDropper": "" "eyeDropper": ""
}, },
"library": { "library": {
@@ -221,7 +223,9 @@
"penMode": "Mòde estilo - empachar lo contact", "penMode": "Mòde estilo - empachar lo contact",
"link": "Apondre/Actualizar lo ligam per una fòrma seleccionada", "link": "Apondre/Actualizar lo ligam per una fòrma seleccionada",
"eraser": "Goma", "eraser": "Goma",
"hand": "Man (aisina de desplaçament de la vista)" "frame": "",
"hand": "Man (aisina de desplaçament de la vista)",
"extraTools": ""
}, },
"headings": { "headings": {
"canvasActions": "Accions del canabàs", "canvasActions": "Accions del canabàs",
+5 -1
View File
@@ -124,6 +124,8 @@
}, },
"statusPublished": "ਪ੍ਰਕਾਸ਼ਤ ਹੈ", "statusPublished": "ਪ੍ਰਕਾਸ਼ਤ ਹੈ",
"sidebarLock": "ਸਾਈਡਬਾਰ ਨੂੰ ਖੁੱਲ੍ਹਾ ਰੱਖੋ", "sidebarLock": "ਸਾਈਡਬਾਰ ਨੂੰ ਖੁੱਲ੍ਹਾ ਰੱਖੋ",
"selectAllElementsInFrame": "",
"removeAllElementsFromFrame": "",
"eyeDropper": "" "eyeDropper": ""
}, },
"library": { "library": {
@@ -221,7 +223,9 @@
"penMode": "ਪੈੱਨ ਮੋਡ - ਟੱਚ ਤੋਂ ਬਚਾਉਂਦਾ ਹੈ", "penMode": "ਪੈੱਨ ਮੋਡ - ਟੱਚ ਤੋਂ ਬਚਾਉਂਦਾ ਹੈ",
"link": "", "link": "",
"eraser": "ਰਬੜ", "eraser": "ਰਬੜ",
"hand": "" "frame": "",
"hand": "",
"extraTools": ""
}, },
"headings": { "headings": {
"canvasActions": "ਕੈਨਵਸ ਦੀਆਂ ਕਾਰਵਾਈਆਂ", "canvasActions": "ਕੈਨਵਸ ਦੀਆਂ ਕਾਰਵਾਈਆਂ",
+36 -36
View File
@@ -1,56 +1,56 @@
{ {
"ar-SA": 80, "ar-SA": 79,
"az-AZ": 20, "az-AZ": 20,
"bg-BG": 54, "bg-BG": 54,
"bn-BD": 60, "bn-BD": 59,
"ca-ES": 88, "ca-ES": 87,
"cs-CZ": 100, "cs-CZ": 98,
"da-DK": 33, "da-DK": 33,
"de-DE": 100, "de-DE": 100,
"el-GR": 93, "el-GR": 92,
"en": 100, "en": 100,
"es-ES": 89, "es-ES": 88,
"eu-ES": 90, "eu-ES": 100,
"fa-IR": 89, "fa-IR": 88,
"fi-FI": 88, "fi-FI": 87,
"fr-FR": 89, "fr-FR": 88,
"gl-ES": 89, "gl-ES": 88,
"he-IL": 87, "he-IL": 86,
"hi-IN": 78, "hi-IN": 77,
"hu-HU": 76, "hu-HU": 76,
"id-ID": 95, "id-ID": 94,
"it-IT": 98, "it-IT": 98,
"ja-JP": 89, "ja-JP": 88,
"kaa": 18, "kaa": 18,
"kab-KAB": 88, "kab-KAB": 87,
"kk-KZ": 21, "kk-KZ": 21,
"km-KH": 95, "km-KH": 94,
"ko-KR": 100, "ko-KR": 100,
"ku-TR": 90, "ku-TR": 100,
"lt-LT": 56, "lt-LT": 55,
"lv-LV": 89, "lv-LV": 88,
"mr-IN": 96, "mr-IN": 95,
"my-MM": 41, "my-MM": 40,
"nb-NO": 100, "nb-NO": 100,
"nl-NL": 83, "nl-NL": 83,
"nn-NO": 77, "nn-NO": 76,
"oc-FR": 87, "oc-FR": 86,
"pa-IN": 90, "pa-IN": 89,
"pl-PL": 100, "pl-PL": 98,
"pt-BR": 99, "pt-BR": 98,
"pt-PT": 95, "pt-PT": 94,
"ro-RO": 100, "ro-RO": 98,
"ru-RU": 100, "ru-RU": 98,
"si-LK": 9, "si-LK": 9,
"sk-SK": 100, "sk-SK": 98,
"sl-SI": 100, "sl-SI": 100,
"sv-SE": 100, "sv-SE": 100,
"ta-IN": 85, "ta-IN": 84,
"th-TH": 39, "th-TH": 38,
"tr-TR": 87, "tr-TR": 86,
"uk-UA": 97, "uk-UA": 96,
"vi-VN": 56, "vi-VN": 56,
"zh-CN": 100, "zh-CN": 100,
"zh-HK": 26, "zh-HK": 25,
"zh-TW": 100 "zh-TW": 100
} }
+5 -1
View File
@@ -124,6 +124,8 @@
}, },
"statusPublished": "Opublikowano", "statusPublished": "Opublikowano",
"sidebarLock": "Panel boczny zawsze otwarty", "sidebarLock": "Panel boczny zawsze otwarty",
"selectAllElementsInFrame": "",
"removeAllElementsFromFrame": "",
"eyeDropper": "Wybierz kolor z płótna" "eyeDropper": "Wybierz kolor z płótna"
}, },
"library": { "library": {
@@ -221,7 +223,9 @@
"penMode": "Tryb pióra — zapobiegaj dotknięciom", "penMode": "Tryb pióra — zapobiegaj dotknięciom",
"link": "Dodaj/aktualizuj link dla wybranego kształtu", "link": "Dodaj/aktualizuj link dla wybranego kształtu",
"eraser": "Gumka", "eraser": "Gumka",
"hand": "Ręka (narzędzie do przesuwania)" "frame": "",
"hand": "Ręka (narzędzie do przesuwania)",
"extraTools": ""
}, },
"headings": { "headings": {
"canvasActions": "Narzędzia", "canvasActions": "Narzędzia",
+5 -1
View File
@@ -124,6 +124,8 @@
}, },
"statusPublished": "Publicado", "statusPublished": "Publicado",
"sidebarLock": "Manter barra lateral aberta", "sidebarLock": "Manter barra lateral aberta",
"selectAllElementsInFrame": "",
"removeAllElementsFromFrame": "",
"eyeDropper": "" "eyeDropper": ""
}, },
"library": { "library": {
@@ -221,7 +223,9 @@
"penMode": "Modo caneta — impede o toque", "penMode": "Modo caneta — impede o toque",
"link": "Adicionar/Atualizar link para uma forma selecionada", "link": "Adicionar/Atualizar link para uma forma selecionada",
"eraser": "Borracha", "eraser": "Borracha",
"hand": "Mão (ferramenta de rolagem)" "frame": "",
"hand": "Mão (ferramenta de rolagem)",
"extraTools": ""
}, },
"headings": { "headings": {
"canvasActions": "Ações da tela", "canvasActions": "Ações da tela",
+5 -1
View File
@@ -124,6 +124,8 @@
}, },
"statusPublished": "Publicado", "statusPublished": "Publicado",
"sidebarLock": "Manter a barra lateral aberta", "sidebarLock": "Manter a barra lateral aberta",
"selectAllElementsInFrame": "",
"removeAllElementsFromFrame": "",
"eyeDropper": "" "eyeDropper": ""
}, },
"library": { "library": {
@@ -221,7 +223,9 @@
"penMode": "Modo caneta - impedir toque", "penMode": "Modo caneta - impedir toque",
"link": "Acrescentar/ Adicionar ligação para uma forma seleccionada", "link": "Acrescentar/ Adicionar ligação para uma forma seleccionada",
"eraser": "Borracha", "eraser": "Borracha",
"hand": "Mão (ferramenta de movimento da tela)" "frame": "",
"hand": "Mão (ferramenta de movimento da tela)",
"extraTools": ""
}, },
"headings": { "headings": {
"canvasActions": "Ações da área de desenho", "canvasActions": "Ações da área de desenho",
+5 -1
View File
@@ -124,6 +124,8 @@
}, },
"statusPublished": "Publicat", "statusPublished": "Publicat",
"sidebarLock": "Păstrează deschisă bara laterală", "sidebarLock": "Păstrează deschisă bara laterală",
"selectAllElementsInFrame": "",
"removeAllElementsFromFrame": "",
"eyeDropper": "Alegere culoare din pânză" "eyeDropper": "Alegere culoare din pânză"
}, },
"library": { "library": {
@@ -221,7 +223,9 @@
"penMode": "Mod stilou împiedică atingerea", "penMode": "Mod stilou împiedică atingerea",
"link": "Adăugare/actualizare URL pentru forma selectată", "link": "Adăugare/actualizare URL pentru forma selectată",
"eraser": "Radieră", "eraser": "Radieră",
"hand": "Mână (instrument de panoramare)" "frame": "",
"hand": "Mână (instrument de panoramare)",
"extraTools": ""
}, },
"headings": { "headings": {
"canvasActions": "Acțiuni pentru pânză", "canvasActions": "Acțiuni pentru pânză",
+5 -1
View File
@@ -124,6 +124,8 @@
}, },
"statusPublished": "Опубликовано", "statusPublished": "Опубликовано",
"sidebarLock": "Держать боковую панель открытой", "sidebarLock": "Держать боковую панель открытой",
"selectAllElementsInFrame": "",
"removeAllElementsFromFrame": "",
"eyeDropper": "Взять образец цвета с холста" "eyeDropper": "Взять образец цвета с холста"
}, },
"library": { "library": {
@@ -221,7 +223,9 @@
"penMode": "Режим пера - предотвращение касания", "penMode": "Режим пера - предотвращение касания",
"link": "Добавить/обновить ссылку для выбранной фигуры", "link": "Добавить/обновить ссылку для выбранной фигуры",
"eraser": "Ластик", "eraser": "Ластик",
"hand": "Рука (перемещение холста)" "frame": "",
"hand": "Рука (перемещение холста)",
"extraTools": ""
}, },
"headings": { "headings": {
"canvasActions": "Операции холста", "canvasActions": "Операции холста",
+5 -1
View File
@@ -124,6 +124,8 @@
}, },
"statusPublished": "", "statusPublished": "",
"sidebarLock": "", "sidebarLock": "",
"selectAllElementsInFrame": "",
"removeAllElementsFromFrame": "",
"eyeDropper": "" "eyeDropper": ""
}, },
"library": { "library": {
@@ -221,7 +223,9 @@
"penMode": "", "penMode": "",
"link": "", "link": "",
"eraser": "", "eraser": "",
"hand": "" "frame": "",
"hand": "",
"extraTools": ""
}, },
"headings": { "headings": {
"canvasActions": "", "canvasActions": "",
+5 -1
View File
@@ -124,6 +124,8 @@
}, },
"statusPublished": "Zverejnené", "statusPublished": "Zverejnené",
"sidebarLock": "Nechať bočný panel otvorený", "sidebarLock": "Nechať bočný panel otvorený",
"selectAllElementsInFrame": "",
"removeAllElementsFromFrame": "",
"eyeDropper": "Vybrať farbu z plátna" "eyeDropper": "Vybrať farbu z plátna"
}, },
"library": { "library": {
@@ -221,7 +223,9 @@
"penMode": "Režim pera zabrániť dotyku", "penMode": "Režim pera zabrániť dotyku",
"link": "Pridať/ Upraviť odkaz pre vybraný tvar", "link": "Pridať/ Upraviť odkaz pre vybraný tvar",
"eraser": "Guma", "eraser": "Guma",
"hand": "Ruka (nástroj pre pohyb plátna)" "frame": "",
"hand": "Ruka (nástroj pre pohyb plátna)",
"extraTools": ""
}, },
"headings": { "headings": {
"canvasActions": "Akcie plátna", "canvasActions": "Akcie plátna",
+5 -1
View File
@@ -124,6 +124,8 @@
}, },
"statusPublished": "Objavljeno", "statusPublished": "Objavljeno",
"sidebarLock": "Obdrži stransko vrstico odprto", "sidebarLock": "Obdrži stransko vrstico odprto",
"selectAllElementsInFrame": "Izberi vse elemente v okvirju",
"removeAllElementsFromFrame": "Izbriši vse elemente v okvirju",
"eyeDropper": "Izberi barvo s platna" "eyeDropper": "Izberi barvo s platna"
}, },
"library": { "library": {
@@ -221,7 +223,9 @@
"penMode": "Način peresa - prepreči dotik", "penMode": "Način peresa - prepreči dotik",
"link": "Dodaj/posodobi povezavo za izbrano obliko", "link": "Dodaj/posodobi povezavo za izbrano obliko",
"eraser": "Radirka", "eraser": "Radirka",
"hand": "Roka (orodje za premikanje)" "frame": "Okvir",
"hand": "Roka (orodje za premikanje)",
"extraTools": "Več orodij"
}, },
"headings": { "headings": {
"canvasActions": "Dejanja za platno", "canvasActions": "Dejanja za platno",
+5 -1
View File
@@ -124,6 +124,8 @@
}, },
"statusPublished": "Publicerad", "statusPublished": "Publicerad",
"sidebarLock": "Håll sidofältet öppet", "sidebarLock": "Håll sidofältet öppet",
"selectAllElementsInFrame": "Markera alla element i rutan",
"removeAllElementsFromFrame": "Ta bort alla element från rutan",
"eyeDropper": "Välj färg från canvas" "eyeDropper": "Välj färg från canvas"
}, },
"library": { "library": {
@@ -221,7 +223,9 @@
"penMode": "Pennläge - förhindra touch", "penMode": "Pennläge - förhindra touch",
"link": "Lägg till / Uppdatera länk för en vald form", "link": "Lägg till / Uppdatera länk för en vald form",
"eraser": "Radergummi", "eraser": "Radergummi",
"hand": "Hand (panoreringsverktyg)" "frame": "Rutverktyg",
"hand": "Hand (panoreringsverktyg)",
"extraTools": "Fler verktyg"
}, },
"headings": { "headings": {
"canvasActions": "Canvas-åtgärder", "canvasActions": "Canvas-åtgärder",
+5 -1
View File
@@ -124,6 +124,8 @@
}, },
"statusPublished": "வெளியிடப்பட்டது", "statusPublished": "வெளியிடப்பட்டது",
"sidebarLock": "பக்கப்பட்டையைத் திறந்தே வை", "sidebarLock": "பக்கப்பட்டையைத் திறந்தே வை",
"selectAllElementsInFrame": "",
"removeAllElementsFromFrame": "",
"eyeDropper": "கித்தானிலிருந்து நிறம் தேர்ந்தெடு" "eyeDropper": "கித்தானிலிருந்து நிறம் தேர்ந்தெடு"
}, },
"library": { "library": {
@@ -221,7 +223,9 @@
"penMode": "", "penMode": "",
"link": "தேர்தெடுத்த வடிவத்திற்குத் தொடுப்பைச் சேர்/ புதுப்பி", "link": "தேர்தெடுத்த வடிவத்திற்குத் தொடுப்பைச் சேர்/ புதுப்பி",
"eraser": "அழிப்பி", "eraser": "அழிப்பி",
"hand": "" "frame": "",
"hand": "",
"extraTools": ""
}, },
"headings": { "headings": {
"canvasActions": "கித்தான் செயல்கள்", "canvasActions": "கித்தான் செயல்கள்",
+5 -1
View File
@@ -124,6 +124,8 @@
}, },
"statusPublished": "เผยแพร่", "statusPublished": "เผยแพร่",
"sidebarLock": "", "sidebarLock": "",
"selectAllElementsInFrame": "",
"removeAllElementsFromFrame": "",
"eyeDropper": "" "eyeDropper": ""
}, },
"library": { "library": {
@@ -221,7 +223,9 @@
"penMode": "", "penMode": "",
"link": "", "link": "",
"eraser": "ยางลบ", "eraser": "ยางลบ",
"hand": "" "frame": "",
"hand": "",
"extraTools": ""
}, },
"headings": { "headings": {
"canvasActions": "", "canvasActions": "",
+5 -1
View File
@@ -124,6 +124,8 @@
}, },
"statusPublished": "Yayınlandı", "statusPublished": "Yayınlandı",
"sidebarLock": "Kenar çubuğu açık kalsın", "sidebarLock": "Kenar çubuğu açık kalsın",
"selectAllElementsInFrame": "",
"removeAllElementsFromFrame": "",
"eyeDropper": "" "eyeDropper": ""
}, },
"library": { "library": {
@@ -221,7 +223,9 @@
"penMode": "Kalem modu - dokunmayı engelle", "penMode": "Kalem modu - dokunmayı engelle",
"link": "Seçilen şekil için bağlantı Ekle/Güncelle", "link": "Seçilen şekil için bağlantı Ekle/Güncelle",
"eraser": "Silgi", "eraser": "Silgi",
"hand": "" "frame": "",
"hand": "",
"extraTools": ""
}, },
"headings": { "headings": {
"canvasActions": "Tuval eylemleri", "canvasActions": "Tuval eylemleri",
+5 -1
View File
@@ -124,6 +124,8 @@
}, },
"statusPublished": "Опубліковано", "statusPublished": "Опубліковано",
"sidebarLock": "Не закривати бокове меню", "sidebarLock": "Не закривати бокове меню",
"selectAllElementsInFrame": "",
"removeAllElementsFromFrame": "",
"eyeDropper": "" "eyeDropper": ""
}, },
"library": { "library": {
@@ -221,7 +223,9 @@
"penMode": "Режим пера - запобігання дотику", "penMode": "Режим пера - запобігання дотику",
"link": "Додати/Оновити посилання для вибраної форми", "link": "Додати/Оновити посилання для вибраної форми",
"eraser": "Очищувач", "eraser": "Очищувач",
"hand": "Рука (інструмент для панорамування)" "frame": "",
"hand": "Рука (інструмент для панорамування)",
"extraTools": ""
}, },
"headings": { "headings": {
"canvasActions": "Дії з полотном", "canvasActions": "Дії з полотном",
+5 -1
View File
@@ -124,6 +124,8 @@
}, },
"statusPublished": "Đã đăng tải", "statusPublished": "Đã đăng tải",
"sidebarLock": "Giữ thanh bên luôn mở", "sidebarLock": "Giữ thanh bên luôn mở",
"selectAllElementsInFrame": "",
"removeAllElementsFromFrame": "",
"eyeDropper": "" "eyeDropper": ""
}, },
"library": { "library": {
@@ -221,7 +223,9 @@
"penMode": "Chế độ bút vẽ - ngăn ngừa chạm nhầm", "penMode": "Chế độ bút vẽ - ngăn ngừa chạm nhầm",
"link": "Thêm/ Chỉnh sửa liên kết cho hình được chọn", "link": "Thêm/ Chỉnh sửa liên kết cho hình được chọn",
"eraser": "Xóa", "eraser": "Xóa",
"hand": "Tay kéo" "frame": "",
"hand": "Tay kéo",
"extraTools": ""
}, },
"headings": { "headings": {
"canvasActions": "Hành động canvas", "canvasActions": "Hành động canvas",
+5 -1
View File
@@ -124,6 +124,8 @@
}, },
"statusPublished": "已发布", "statusPublished": "已发布",
"sidebarLock": "侧边栏常驻", "sidebarLock": "侧边栏常驻",
"selectAllElementsInFrame": "选择画框中的所有元素",
"removeAllElementsFromFrame": "删除画框中的所有元素",
"eyeDropper": "从画布上取色" "eyeDropper": "从画布上取色"
}, },
"library": { "library": {
@@ -221,7 +223,9 @@
"penMode": "笔模式 避免误触", "penMode": "笔模式 避免误触",
"link": "为选中的形状添加/更新链接", "link": "为选中的形状添加/更新链接",
"eraser": "橡皮", "eraser": "橡皮",
"hand": "抓手(平移工具" "frame": "画框工具",
"hand": "抓手(平移工具)",
"extraTools": "更多工具"
}, },
"headings": { "headings": {
"canvasActions": "画布动作", "canvasActions": "画布动作",
+5 -1
View File
@@ -124,6 +124,8 @@
}, },
"statusPublished": "", "statusPublished": "",
"sidebarLock": "", "sidebarLock": "",
"selectAllElementsInFrame": "",
"removeAllElementsFromFrame": "",
"eyeDropper": "" "eyeDropper": ""
}, },
"library": { "library": {
@@ -221,7 +223,9 @@
"penMode": "", "penMode": "",
"link": "", "link": "",
"eraser": "", "eraser": "",
"hand": "" "frame": "",
"hand": "",
"extraTools": ""
}, },
"headings": { "headings": {
"canvasActions": "畫布動作", "canvasActions": "畫布動作",
+5 -1
View File
@@ -124,6 +124,8 @@
}, },
"statusPublished": "已發布", "statusPublished": "已發布",
"sidebarLock": "側欄維持開啟", "sidebarLock": "側欄維持開啟",
"selectAllElementsInFrame": "選取框架內的所有元素",
"removeAllElementsFromFrame": "從框架內移除所有元素",
"eyeDropper": "從畫布中選取顏色" "eyeDropper": "從畫布中選取顏色"
}, },
"library": { "library": {
@@ -221,7 +223,9 @@
"penMode": "筆模式 - 避免觸摸", "penMode": "筆模式 - 避免觸摸",
"link": "為所選的形狀增加\b/更新連結", "link": "為所選的形狀增加\b/更新連結",
"eraser": "橡皮擦", "eraser": "橡皮擦",
"hand": "手形(平移工具" "frame": "框架工具",
"hand": "手形(平移工具)",
"extraTools": "更多工具"
}, },
"headings": { "headings": {
"canvasActions": "canvas 動作", "canvasActions": "canvas 動作",
+3 -1
View File
@@ -15,6 +15,8 @@ Please add the latest change on the top under the correct section.
### Features ### Features
- Add support for `opts.fitToViewport` and `opts.viewportZoomFactor` in the [`ExcalidrawAPI.scrollToContent`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props/ref#scrolltocontent) API. [#6581](https://github.com/excalidraw/excalidraw/pull/6581).
- Properly sanitize element `link` urls. [#6728](https://github.com/excalidraw/excalidraw/pull/6728).
- Sidebar component now supports tabs — for more detailed description of new behavior and breaking changes, see the linked PR. [#6213](https://github.com/excalidraw/excalidraw/pull/6213) - Sidebar component now supports tabs — for more detailed description of new behavior and breaking changes, see the linked PR. [#6213](https://github.com/excalidraw/excalidraw/pull/6213)
- Exposed `DefaultSidebar` component to allow modifying the default sidebar, such as adding custom tabs to it. [#6213](https://github.com/excalidraw/excalidraw/pull/6213) - Exposed `DefaultSidebar` component to allow modifying the default sidebar, such as adding custom tabs to it. [#6213](https://github.com/excalidraw/excalidraw/pull/6213)
@@ -63,7 +65,7 @@ Please add the latest change on the top under the correct section.
### Features ### Features
- [`ExcalidrawAPI.scrolToContent`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props/ref#scrolltocontent) has new opts object allowing you to fit viewport to content, and animate the scrolling. [#6319](https://github.com/excalidraw/excalidraw/pull/6319) - [`ExcalidrawAPI.scrollToContent`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props/ref#scrolltocontent) has new opts object allowing you to fit viewport to content, and animate the scrolling. [#6319](https://github.com/excalidraw/excalidraw/pull/6319)
- Expose `useI18n()` hook return an object containing `t()` i18n helper and current `langCode`. You can use this in components you render as `<Excalidraw>` children to render any of our i18n locale strings. [#6224](https://github.com/excalidraw/excalidraw/pull/6224) - Expose `useI18n()` hook return an object containing `t()` i18n helper and current `langCode`. You can use this in components you render as `<Excalidraw>` children to render any of our i18n locale strings. [#6224](https://github.com/excalidraw/excalidraw/pull/6224)

Some files were not shown because too many files have changed in this diff Show More