From 28691e14b1199d9e2890fa6410f26c61e62c021e Mon Sep 17 00:00:00 2001 From: Christopher Tangonan <161169629+cTangonan123@users.noreply.github.com> Date: Mon, 26 Jan 2026 09:52:13 -0800 Subject: [PATCH 01/32] fix: Bound arrow elements for distribute and wyswig updates (#10702) * fix: update distributeElements to updateBoundElements Co-authored-by: Anvi Kudaraya * fix: apply updateBoundElements when bound text extends past container height Co-authored-by: Anvi Kudaraya --------- Co-authored-by: Anvi Kudaraya --- packages/element/src/distribute.ts | 29 +++++++++++++------ .../excalidraw/actions/actionDistribute.tsx | 1 + packages/excalidraw/wysiwyg/textWysiwyg.tsx | 3 ++ 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/packages/element/src/distribute.ts b/packages/element/src/distribute.ts index add3522acc..bb9c2708d2 100644 --- a/packages/element/src/distribute.ts +++ b/packages/element/src/distribute.ts @@ -1,10 +1,12 @@ import type { AppState } from "@excalidraw/excalidraw/types"; +import { updateBoundElements } from "./binding"; import { getCommonBoundingBox } from "./bounds"; -import { newElementWith } from "./mutateElement"; import { getSelectedElementsByGroup } from "./groups"; +import type { Scene } from "./Scene"; + import type { ElementsMap, ExcalidrawElement } from "./types"; export interface Distribution { @@ -17,6 +19,7 @@ export const distributeElements = ( elementsMap: ElementsMap, distribution: Distribution, appState: Readonly, + scene: Scene, ): ExcalidrawElement[] => { const [start, mid, end, extent] = distribution.axis === "x" @@ -66,12 +69,16 @@ export const distributeElements = ( translation[distribution.axis] = pos - box[mid]; } - return group.map((element) => - newElementWith(element, { + return group.map((element) => { + const updatedElement = scene.mutateElement(element, { x: element.x + translation.x, y: element.y + translation.y, - }), - ); + }); + updateBoundElements(element, scene, { + simultaneouslyUpdated: group, + }); + return updatedElement; + }); }); } @@ -90,11 +97,15 @@ export const distributeElements = ( pos += step; pos += box[extent]; - return group.map((element) => - newElementWith(element, { + return group.map((element) => { + const updatedElement = scene.mutateElement(element, { x: element.x + translation.x, y: element.y + translation.y, - }), - ); + }); + updateBoundElements(element, scene, { + simultaneouslyUpdated: group, + }); + return updatedElement; + }); }); }; diff --git a/packages/excalidraw/actions/actionDistribute.tsx b/packages/excalidraw/actions/actionDistribute.tsx index 88e085f1de..6c75603478 100644 --- a/packages/excalidraw/actions/actionDistribute.tsx +++ b/packages/excalidraw/actions/actionDistribute.tsx @@ -58,6 +58,7 @@ const distributeSelectedElements = ( app.scene.getNonDeletedElementsMap(), distribution, appState, + app.scene, ); const updatedElementsMap = arrayToMap(updatedElements); diff --git a/packages/excalidraw/wysiwyg/textWysiwyg.tsx b/packages/excalidraw/wysiwyg/textWysiwyg.tsx index 90a4e101e7..52c80e9c65 100644 --- a/packages/excalidraw/wysiwyg/textWysiwyg.tsx +++ b/packages/excalidraw/wysiwyg/textWysiwyg.tsx @@ -14,6 +14,7 @@ import { import { originalContainerCache, + updateBoundElements, updateOriginalContainerCache, } from "@excalidraw/element"; @@ -208,6 +209,7 @@ export const textWysiwyg = ({ ); app.scene.mutateElement(container, { height: targetContainerHeight }); + updateBoundElements(container, app.scene); return; } else if ( // autoshrink container height until original container height @@ -221,6 +223,7 @@ export const textWysiwyg = ({ container.type, ); app.scene.mutateElement(container, { height: targetContainerHeight }); + updateBoundElements(container, app.scene); } else { const { x, y } = computeBoundTextPosition( container, From dfdd994dbbefc0562c0c5e42f267f23ae8d9d62d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rk=20Tolm=C3=A1cs?= Date: Mon, 26 Jan 2026 20:50:44 +0100 Subject: [PATCH 02/32] perf: Cache hits in collision detection (#10648) * feat: Cache hits and scene lookups Signed-off-by: Mark Tolmacs * chore: Remove debug Signed-off-by: Mark Tolmacs * fix: Consider hit threshold and inside override too Signed-off-by: Mark Tolmacs * chore: Remove Map caching Signed-off-by: Mark Tolmacs * fix: incorrect threshold Signed-off-by: Mark Tolmacs * fix: threshold setting Signed-off-by: Mark Tolmacs * fix: Hit caching Signed-off-by: Mark Tolmacs * fix: cache override Signed-off-by: Mark Tolmacs --------- Signed-off-by: Mark Tolmacs --- packages/element/src/collision.ts | 35 +++- packages/element/tests/collision.test.tsx | 184 +++++++++++++++++- .../tests/linearElementEditor.test.tsx | 2 +- 3 files changed, 217 insertions(+), 4 deletions(-) diff --git a/packages/element/src/collision.ts b/packages/element/src/collision.ts index a96c3ebc14..d93363a4d8 100644 --- a/packages/element/src/collision.ts +++ b/packages/element/src/collision.ts @@ -105,6 +105,12 @@ export type HitTestArgs = { overrideShouldTestInside?: boolean; }; +let cachedPoint: GlobalPoint | null = null; +let cachedElement: WeakRef | null = null; +let cachedThreshold: number = Infinity; +let cachedHit: boolean = false; +let cachedOverrideShouldTestInside = false; + export const hitElementItself = ({ point, element, @@ -113,6 +119,24 @@ export const hitElementItself = ({ frameNameBound = null, overrideShouldTestInside = false, }: HitTestArgs) => { + // Return cached result if the same point and element version is tested again + if ( + cachedPoint && + pointsEqual(point, cachedPoint) && + cachedThreshold <= threshold && + overrideShouldTestInside === cachedOverrideShouldTestInside + ) { + const derefElement = cachedElement?.deref(); + if ( + derefElement && + derefElement.id === element.id && + derefElement.version === element.version && + derefElement.versionNonce === element.versionNonce + ) { + return cachedHit; + } + } + // Hit test against a frame's name const hitFrameName = frameNameBound ? isPointWithinBounds( @@ -153,7 +177,16 @@ export const hitElementItself = ({ isPointOnElementOutline(point, element, elementsMap, threshold) : isPointOnElementOutline(point, element, elementsMap, threshold); - return hitElement || hitFrameName; + const result = hitElement || hitFrameName; + + // Cache end result + cachedPoint = point; + cachedElement = new WeakRef(element); + cachedThreshold = threshold; + cachedOverrideShouldTestInside = overrideShouldTestInside; + cachedHit = result; + + return result; }; export const hitElementBoundingBox = ( diff --git a/packages/element/tests/collision.test.tsx b/packages/element/tests/collision.test.tsx index 72996bdb1f..4061a16cb6 100644 --- a/packages/element/tests/collision.test.tsx +++ b/packages/element/tests/collision.test.tsx @@ -1,9 +1,12 @@ +import { arrayToMap } from "@excalidraw/common"; import { type GlobalPoint, type LocalPoint, pointFrom } from "@excalidraw/math"; import { Excalidraw } from "@excalidraw/excalidraw"; +import { API } from "@excalidraw/excalidraw/tests/helpers/api"; import { UI } from "@excalidraw/excalidraw/tests/helpers/ui"; import "@excalidraw/utils/test-utils"; import { render } from "@excalidraw/excalidraw/tests/test-utils"; +import * as distance from "../src/distance"; import { hitElementItself } from "../src/collision"; describe("check rotated elements can be hit:", () => { @@ -25,8 +28,6 @@ describe("check rotated elements can be hit:", () => { [-4, -302], ] as LocalPoint[], }); - //const p = [120, -211]; - //const p = [0, 13]; const hit = hitElementItself({ point: pointFrom(88, -68), element: window.h.elements[0], @@ -36,3 +37,182 @@ describe("check rotated elements can be hit:", () => { expect(hit).toBe(true); }); }); + +describe("hitElementItself cache", () => { + beforeEach(async () => { + // reset cache + hitElementItself({ + point: pointFrom(50, 50), + element: API.createElement({ + type: "rectangle", + x: 0, + y: 0, + width: 100, + height: 100, + backgroundColor: "#ffffff", + }), + threshold: Infinity, + elementsMap: new Map([]), + }); + + localStorage.clear(); + await render(); + }); + + it("reuses cached result when threshold increases", () => { + const element = API.createElement({ + type: "rectangle", + x: 0, + y: 0, + width: 100, + height: 100, + backgroundColor: "#ffffff", + }); + const elementsMap = arrayToMap([element]); + const point = pointFrom(100.5, 50); + + const distanceSpy = jest.spyOn(distance, "distanceToElement"); + + expect( + hitElementItself({ + point, + element, + threshold: 1, + elementsMap, + }), + ).toBe(true); + + expect(distanceSpy).toHaveBeenCalledTimes(1); + + expect( + hitElementItself({ + point, + element, + threshold: 10, + elementsMap, + }), + ).toBe(true); + + expect(distanceSpy).toHaveBeenCalledTimes(1); + + distanceSpy.mockRestore(); + }); + + it("does not reuse cache when threshold decreases", () => { + const element = API.createElement({ + type: "rectangle", + x: 0, + y: 0, + width: 100, + height: 100, + backgroundColor: "transparent", + }); + const elementsMap = arrayToMap([element]); + const point = pointFrom(105, 50); + + const distanceSpy = jest.spyOn(distance, "distanceToElement"); + + expect( + hitElementItself({ + point, + element, + threshold: 10, + elementsMap, + }), + ).toBe(true); + + expect(distanceSpy).toHaveBeenCalledTimes(1); + + expect( + hitElementItself({ + point, + element, + threshold: 6, + elementsMap, + }), + ).toBe(true); + + expect(distanceSpy).toHaveBeenCalledTimes(2); + distanceSpy.mockRestore(); + }); + + it("invalidates cache when element version changes", () => { + const element = API.createElement({ + type: "rectangle", + x: 0, + y: 0, + width: 100, + height: 100, + backgroundColor: "#ffffff", + }); + const elementsMap = arrayToMap([element]); + const point = pointFrom(100.5, 50); + + const distanceSpy = jest.spyOn(distance, "distanceToElement"); + + expect( + hitElementItself({ + point, + element, + threshold: 1, + elementsMap, + }), + ).toBe(true); + + expect(distanceSpy).toHaveBeenCalledTimes(1); + + const movedElement = { + ...element, + version: element.version + 1, + versionNonce: element.versionNonce + 1, + }; + + expect( + hitElementItself({ + point, + element: movedElement, + threshold: 1, + elementsMap, + }), + ).toBe(true); + + expect(distanceSpy).toHaveBeenCalledTimes(2); + distanceSpy.mockRestore(); + }); + + it("override does not affect caching", () => { + const element = API.createElement({ + type: "rectangle", + x: 0, + y: 0, + width: 100, + height: 100, + backgroundColor: "transparent", + }); + const elementsMap = arrayToMap([element]); + const point = pointFrom(50, 50); + + const distanceSpy = jest.spyOn(distance, "distanceToElement"); + + expect( + hitElementItself({ + point, + element, + threshold: 10, + elementsMap, + }), + ).toBe(false); + + expect(distanceSpy).toHaveBeenCalledTimes(1); + + expect( + hitElementItself({ + point, + element, + threshold: 10, + elementsMap, + overrideShouldTestInside: true, + }), + ).toBe(true); + }); +}); diff --git a/packages/element/tests/linearElementEditor.test.tsx b/packages/element/tests/linearElementEditor.test.tsx index 5759c591dd..5b31892b4d 100644 --- a/packages/element/tests/linearElementEditor.test.tsx +++ b/packages/element/tests/linearElementEditor.test.tsx @@ -218,7 +218,7 @@ describe("Test Linear Elements", () => { // drag line from midpoint drag(midpoint, pointFrom(midpoint[0] + delta, midpoint[1] + delta)); expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(`8`); - expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`); + expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`); expect(line.points.length).toEqual(3); expect(line.points).toMatchInlineSnapshot(` [ From 54fa0c908969236908aab47b908fb648ddc01cb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rk=20Tolm=C3=A1cs?= Date: Mon, 26 Jan 2026 20:52:13 +0100 Subject: [PATCH 03/32] fix: Increase iteration on curve intersection calc (#10707) Signed-off-by: Mark Tolmacs --- .../data/__snapshots__/restore.test.ts.snap | 47 ------------------- packages/math/src/curve.ts | 20 +------- 2 files changed, 1 insertion(+), 66 deletions(-) diff --git a/packages/excalidraw/tests/data/__snapshots__/restore.test.ts.snap b/packages/excalidraw/tests/data/__snapshots__/restore.test.ts.snap index 802edcc373..e3c0581522 100644 --- a/packages/excalidraw/tests/data/__snapshots__/restore.test.ts.snap +++ b/packages/excalidraw/tests/data/__snapshots__/restore.test.ts.snap @@ -428,50 +428,3 @@ exports[`restoreElements > should restore text element correctly with unknown fo "y": 0, } `; - -exports[`restoreElements > should strip arrow binding if repair throws 1`] = ` -{ - "angle": 0, - "backgroundColor": "transparent", - "boundElements": [], - "customData": undefined, - "elbowed": false, - "endArrowhead": null, - "endBinding": null, - "fillStyle": "solid", - "frameId": null, - "groupIds": [], - "height": 100, - "id": "id-arrow01", - "index": "a0", - "isDeleted": false, - "link": null, - "locked": false, - "opacity": 100, - "points": [ - [ - 0, - 0, - ], - [ - 100, - 100, - ], - ], - "roughness": 1, - "roundness": null, - "seed": Any, - "startArrowhead": null, - "startBinding": null, - "strokeColor": "#1e1e1e", - "strokeStyle": "solid", - "strokeWidth": 2, - "type": "arrow", - "updated": 1, - "version": 2, - "versionNonce": Any, - "width": 100, - "x": 0, - "y": 0, -} -`; diff --git a/packages/math/src/curve.ts b/packages/math/src/curve.ts index 55db76877e..32f537f434 100644 --- a/packages/math/src/curve.ts +++ b/packages/math/src/curve.ts @@ -1,7 +1,6 @@ import { isPoint, pointDistance, pointFrom, pointFromVector } from "./point"; import { vector, vectorNormal, vectorNormalize, vectorScale } from "./vector"; import { LegendreGaussN24CValues, LegendreGaussN24TValues } from "./constants"; -import { lineSegment, lineSegmentIntersectionPoints } from "./segment"; import type { Curve, GlobalPoint, LineSegment, LocalPoint } from "./types"; @@ -139,7 +138,7 @@ const calculate = ( l: LineSegment, c: Curve, ) => { - const solution = solveWithAnalyticalJacobian(c, l, t0, s0, 1e-2, 3); + const solution = solveWithAnalyticalJacobian(c, l, t0, s0, 1e-2, 4); if (!solution) { return null; @@ -175,23 +174,6 @@ export function curveIntersectLineSegment< return [solution]; } - // Fallback: approximate the curve with short segments to catch near-endpoint hits. - const startHit = lineSegmentIntersectionPoints( - lineSegment(bezierEquation(c, 0), bezierEquation(c, 1 / 20)), - l, - ); - if (startHit) { - return [startHit]; - } - - const endHit = lineSegmentIntersectionPoints( - lineSegment(bezierEquation(c, 19 / 20), bezierEquation(c, 1)), - l, - ); - if (endHit) { - return [endHit]; - } - return []; } From 6a891365b968ffa2b54b1d03be06674cb2757ea1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rk=20Tolm=C3=A1cs?= Date: Mon, 26 Jan 2026 20:55:08 +0100 Subject: [PATCH 04/32] fix: Arrow endpoint offset (#10706) Signed-off-by: Mark Tolmacs --- packages/element/src/linearElementEditor.ts | 1 - packages/excalidraw/actions/actionFinalize.tsx | 10 +++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/element/src/linearElementEditor.ts b/packages/element/src/linearElementEditor.ts index 62f5ebbff7..00bf516350 100644 --- a/packages/element/src/linearElementEditor.ts +++ b/packages/element/src/linearElementEditor.ts @@ -724,7 +724,6 @@ export class LinearElementEditor { ? [pointerDownState.lastClickedPoint] : selectedPointsIndices, isDragging: false, - pointerOffset: { x: 0, y: 0 }, customLineAngle: null, initialState: { ...editingLinearElement.initialState, diff --git a/packages/excalidraw/actions/actionFinalize.tsx b/packages/excalidraw/actions/actionFinalize.tsx index 1dd6884259..e0f59a5655 100644 --- a/packages/excalidraw/actions/actionFinalize.tsx +++ b/packages/excalidraw/actions/actionFinalize.tsx @@ -98,7 +98,10 @@ export const actionFinalize = register({ map.set(index, { point: LinearElementEditor.pointFromAbsoluteCoords( element, - pointFrom(sceneCoords.x, sceneCoords.y), + pointFrom( + sceneCoords.x - linearElementEditor.pointerOffset.x, + sceneCoords.y - linearElementEditor.pointerOffset.y, + ), elementsMap, ), }); @@ -109,8 +112,8 @@ export const actionFinalize = register({ bindOrUnbindBindingElement( element, draggedPoints, - sceneCoords.x, - sceneCoords.y, + sceneCoords.x - linearElementEditor.pointerOffset.x, + sceneCoords.y - linearElementEditor.pointerOffset.y, scene, appState, { @@ -173,6 +176,7 @@ export const actionFinalize = register({ ...linearElementEditor.initialState, lastClickedPoint: -1, }, + pointerOffset: { x: 0, y: 0 }, }, selectionElement: null, suggestedBinding: null, From f5cf81ce42276ef8148b7cd951274025648476b8 Mon Sep 17 00:00:00 2001 From: David Luzar <5153846+dwelle@users.noreply.github.com> Date: Wed, 28 Jan 2026 22:35:39 +0100 Subject: [PATCH 05/32] feat: prevent pasting excalidraw into textarea & paste element text if avail (#10710) * feat: prevent pasting excalidraw json into textarea & paste element text if avail Co-authored-by: Ashutosh Kumar <130897584+codeaashu@users.noreply.github.com> * fix FF --------- Co-authored-by: Ashutosh Kumar <130897584+codeaashu@users.noreply.github.com> --- packages/common/src/constants.ts | 1 + packages/excalidraw/clipboard.ts | 100 ++++++++++++++------ packages/excalidraw/wysiwyg/textWysiwyg.tsx | 64 ++++++++++++- 3 files changed, 134 insertions(+), 31 deletions(-) diff --git a/packages/common/src/constants.ts b/packages/common/src/constants.ts index 23d5f426e6..03978584cb 100644 --- a/packages/common/src/constants.ts +++ b/packages/common/src/constants.ts @@ -251,6 +251,7 @@ export const STRING_MIME_TYPES = { json: "application/json", // excalidraw data excalidraw: "application/vnd.excalidraw+json", + excalidrawClipboard: "application/vnd.excalidraw.clipboard+json", // LEGACY: fully-qualified library JSON data excalidrawlib: "application/vnd.excalidrawlib+json", // list of excalidraw library item ids diff --git a/packages/excalidraw/clipboard.ts b/packages/excalidraw/clipboard.ts index 1e6d74eca3..55fa2b92c3 100644 --- a/packages/excalidraw/clipboard.ts +++ b/packages/excalidraw/clipboard.ts @@ -204,8 +204,13 @@ export const copyToClipboard = async ( /** supply if available to make the operation more certain to succeed */ clipboardEvent?: ClipboardEvent | null, ) => { + const json = serializeAsClipboardJSON({ elements, files }); + await copyTextToSystemClipboard( - serializeAsClipboardJSON({ elements, files }), + { + [MIME_TYPES.excalidrawClipboard]: json, + [MIME_TYPES.text]: json, + }, clipboardEvent, ); }; @@ -401,7 +406,7 @@ export type ParsedDataTransferFile = Extract< { kind: "file" } >; -type ParsedDataTranferList = ParsedDataTransferItem[] & { +export type ParsedDataTranferList = ParsedDataTransferItem[] & { /** * Only allows filtering by known `string` data types, since `file` * types can have multiple items of the same type (e.g. multiple image files) @@ -452,6 +457,29 @@ const getDataTransferFiles = function ( ); }; +/** @returns list of MIME types, synchronously */ +export const parseDataTransferEventMimeTypes = ( + event: ClipboardEvent | DragEvent | React.DragEvent, +): Set => { + let items: DataTransferItemList | undefined = undefined; + + if (isClipboardEvent(event)) { + items = event.clipboardData?.items; + } else { + items = event.dataTransfer?.items; + } + + const types: Set = new Set(); + + for (const item of Array.from(items || [])) { + if (!types.has(item.type)) { + types.add(item.type); + } + } + + return types; +}; + export const parseDataTransferEvent = async ( event: ClipboardEvent | DragEvent | React.DragEvent, ): Promise => { @@ -460,8 +488,7 @@ export const parseDataTransferEvent = async ( if (isClipboardEvent(event)) { items = event.clipboardData?.items; } else { - const dragEvent = event; - items = dragEvent.dataTransfer?.items; + items = event.dataTransfer?.items; } const dataItems = ( @@ -567,7 +594,7 @@ export const copyBlobToClipboardAsPng = async (blob: Blob | Promise) => { // ClipboardItem constructor, but throws on an unrelated MIME type error. // So we need to await this and fallback to awaiting the blob if applicable. await navigator.clipboard.write([ - new window.ClipboardItem({ + new ClipboardItem({ [MIME_TYPES.png]: blob, }), ]); @@ -576,7 +603,7 @@ export const copyBlobToClipboardAsPng = async (blob: Blob | Promise) => { // with resolution value instead if (isPromiseLike(blob)) { await navigator.clipboard.write([ - new window.ClipboardItem({ + new ClipboardItem({ [MIME_TYPES.png]: await blob, }), ]); @@ -586,37 +613,56 @@ export const copyBlobToClipboardAsPng = async (blob: Blob | Promise) => { } }; -export const copyTextToSystemClipboard = async ( - text: string | null, +export const copyTextToSystemClipboard = async < + MimeType extends ValueOf, +>( + text: string | { [K in MimeType]: string } | null, clipboardEvent?: ClipboardEvent | null, ) => { - // (1) first try using Async Clipboard API - if (probablySupportsClipboardWriteText) { + text = text || ""; + + const entries = Object.entries( + typeof text === "string" ? { [MIME_TYPES.text]: text } : text, + ); + + // (1) if we have clipboardEvent, try using it first as it's the most + // versatile + try { + if (clipboardEvent) { + for (const [mimeType, value] of entries) { + clipboardEvent.clipboardData?.setData(mimeType, value); + if (clipboardEvent.clipboardData?.getData(mimeType) !== value) { + throw new Error("Failed to setData on clipboardEvent"); + } + } + } + return; + } catch (error: any) { + console.error(error); + } + + let plainTextEntry = entries.find( + ([mimeType]) => mimeType === MIME_TYPES.text, + ); + + // (2) if we don't have access to clipboardEvent, or that fails, + // at least try setting text/plain via navigator.clipboard.writeText + // (navigator.clipboard.write doesn't work with non-standard mime types) + if (probablySupportsClipboardWriteText && plainTextEntry) { try { // NOTE: doesn't work on FF on non-HTTPS domains, or when document // not focused - await navigator.clipboard.writeText(text || ""); - return; + await navigator.clipboard.writeText(plainTextEntry[1]); + + // invalidate it so we don't write it again below + plainTextEntry = undefined; } catch (error: any) { console.error(error); } } - // (2) if fails and we have access to ClipboardEvent, use plain old setData() - try { - if (clipboardEvent) { - clipboardEvent.clipboardData?.setData(MIME_TYPES.text, text || ""); - if (clipboardEvent.clipboardData?.getData(MIME_TYPES.text) !== text) { - throw new Error("Failed to setData on clipboardEvent"); - } - return; - } - } catch (error: any) { - console.error(error); - } - - // (3) if that fails, use document.execCommand - if (!copyTextViaExecCommand(text)) { + // (3) if previous fails, use document.execCommand + if (plainTextEntry && !copyTextViaExecCommand(plainTextEntry[1])) { throw new Error("Error copying to clipboard."); } }; diff --git a/packages/excalidraw/wysiwyg/textWysiwyg.tsx b/packages/excalidraw/wysiwyg/textWysiwyg.tsx index 52c80e9c65..27086e07b9 100644 --- a/packages/excalidraw/wysiwyg/textWysiwyg.tsx +++ b/packages/excalidraw/wysiwyg/textWysiwyg.tsx @@ -13,6 +13,7 @@ import { } from "@excalidraw/common"; import { + getTextFromElements, originalContainerCache, updateBoundElements, updateOriginalContainerCache, @@ -49,7 +50,11 @@ import type { import { actionSaveToActiveFile } from "../actions"; -import { parseDataTransferEvent } from "../clipboard"; +import { + parseClipboard, + parseDataTransferEvent, + parseDataTransferEventMimeTypes, +} from "../clipboard"; import { actionDecreaseFontSize, actionIncreaseFontSize, @@ -60,6 +65,8 @@ import { actionZoomOut, } from "../actions/actionCanvas"; +import type { ParsedDataTranferList } from "../clipboard"; + import type App from "../components/App"; import type { AppState } from "../types"; @@ -328,9 +335,58 @@ export const textWysiwyg = ({ if (onChange) { editable.onpaste = async (event) => { - const textItem = (await parseDataTransferEvent(event)).findByType( - MIME_TYPES.text, - ); + // we need to synchronously get the MIME types so we can preventDefault() + // in the same tick (FF requires that) + const mimeTypes = parseDataTransferEventMimeTypes(event); + + let dataList: ParsedDataTranferList | null = null; + + // when copy/pasting excalidraw elements, only paste the text content + // + // Note that these custom MIME types only work within the same family + // of browsers, so won't work e.g. between chrome and firefox. We could + // parse the text/plain for existence of excalidraw instead, but this + // is an edge case + if ( + mimeTypes.has(MIME_TYPES.excalidrawClipboard) || + mimeTypes.has(MIME_TYPES.excalidraw) + ) { + // must be called in the same tick + event.preventDefault(); + + dataList = await parseDataTransferEvent(event); + + try { + const parsed = await parseClipboard(dataList); + + if (parsed.elements) { + const text = getTextFromElements(parsed.elements); + if (text) { + const { selectionStart, selectionEnd, value } = editable; + + editable.value = + value.slice(0, selectionStart) + + text + + value.slice(selectionEnd); + + const newPos = selectionStart + text.length; + editable.selectionStart = editable.selectionEnd = newPos; + + editable.dispatchEvent(new Event("input")); + } + } + + // if excalidraw elements don't contain any text elements, + // don't paste anything + return; + } catch { + console.warn("failed to parse excalidraw clipboard data"); + } + } + + dataList = dataList || (await parseDataTransferEvent(event)); + + const textItem = dataList.findByType(MIME_TYPES.text); if (!textItem) { return; } From 802cde35019354ab3c4749ef1ae7ea71b404e31b Mon Sep 17 00:00:00 2001 From: David Luzar <5153846+dwelle@users.noreply.github.com> Date: Fri, 30 Jan 2026 16:28:36 +0100 Subject: [PATCH 06/32] feat: support customizing TTD welcome screen (#10719) * feat: support customizing TTD welcome screen * remove debug --- .../components/TTDDialog/Chat/Chat.scss | 4 ++-- .../TTDDialog/Chat/ChatInterface.tsx | 22 +++++++++---------- .../TTDDialog/Chat/TTDChatPanel.tsx | 8 +++---- .../components/TTDDialog/TTDDialog.tsx | 7 ++++++ .../TTDDialog/TTDWelcomeMessage.tsx | 11 ++++++++++ .../components/TTDDialog/TextToDiagram.tsx | 6 +++++ .../excalidraw/components/TTDDialog/types.ts | 5 +++++ 7 files changed, 44 insertions(+), 19 deletions(-) create mode 100644 packages/excalidraw/components/TTDDialog/TTDWelcomeMessage.tsx diff --git a/packages/excalidraw/components/TTDDialog/Chat/Chat.scss b/packages/excalidraw/components/TTDDialog/Chat/Chat.scss index 01b5a96012..63671ed66e 100644 --- a/packages/excalidraw/components/TTDDialog/Chat/Chat.scss +++ b/packages/excalidraw/components/TTDDialog/Chat/Chat.scss @@ -48,14 +48,14 @@ $verticalBreakpoint: 861px; } } - &__empty-state { + &__welcome-screen { display: flex; align-items: center; justify-content: center; height: 100%; min-height: 200px; - &-content { + &__welcome-message { text-align: center; h3 { diff --git a/packages/excalidraw/components/TTDDialog/Chat/ChatInterface.tsx b/packages/excalidraw/components/TTDDialog/Chat/ChatInterface.tsx index 312c49ac9c..1f0a302351 100644 --- a/packages/excalidraw/components/TTDDialog/Chat/ChatInterface.tsx +++ b/packages/excalidraw/components/TTDDialog/Chat/ChatInterface.tsx @@ -6,6 +6,8 @@ import { InlineIcon } from "../../InlineIcon"; import { t } from "../../../i18n"; +import { TTDWelcomeMessage } from "../TTDWelcomeMessage"; + import { ChatMessage } from "./ChatMessage"; import type { TChat, TTTDDialog } from "../types"; @@ -20,13 +22,13 @@ export const ChatInterface = ({ onGenerate, isGenerating, rateLimits, - placeholder, onAbort, onMermaidTabClick, onAiRepairClick, onDeleteMessage, onInsertMessage, onRetry, + renderWelcomeScreen, renderWarning, }: { chatId: string; @@ -41,17 +43,13 @@ export const ChatInterface = ({ } | null; onViewAsMermaid?: () => void; generatedResponse?: string | null; - placeholder: { - title: string; - description: string; - hint: string; - }; onAbort?: () => void; onMermaidTabClick?: (message: TChat.ChatMessage) => void; onAiRepairClick?: (message: TChat.ChatMessage) => void; onDeleteMessage?: (messageId: string) => void; onInsertMessage?: (message: TChat.ChatMessage) => void; onRetry?: (message: TChat.ChatMessage) => void; + renderWelcomeScreen?: TTTDDialog.renderWelcomeScreen; renderWarning?: TTTDDialog.renderWarning; }) => { const messagesEndRef = useRef(null); @@ -113,12 +111,12 @@ export const ChatInterface = ({
{messages.length === 0 ? ( -
-
-

{placeholder.title}

-

{placeholder.description}

-

{placeholder.hint}

-
+
+ {renderWelcomeScreen ? ( + renderWelcomeScreen({ rateLimits: rateLimits ?? null }) + ) : ( + + )}
) : ( messages.map((message, index) => ( diff --git a/packages/excalidraw/components/TTDDialog/Chat/TTDChatPanel.tsx b/packages/excalidraw/components/TTDDialog/Chat/TTDChatPanel.tsx index a9e66df6e4..25aa30df88 100644 --- a/packages/excalidraw/components/TTDDialog/Chat/TTDChatPanel.tsx +++ b/packages/excalidraw/components/TTDDialog/Chat/TTDChatPanel.tsx @@ -40,6 +40,7 @@ export const TTDChatPanel = ({ onInsertMessage, onRetry, onViewAsMermaid, + renderWelcomeScreen, renderWarning, }: { chatId: string; @@ -68,6 +69,7 @@ export const TTDChatPanel = ({ onViewAsMermaid: () => void; + renderWelcomeScreen?: TTTDDialog.renderWelcomeScreen; renderWarning?: TTTDDialog.renderWarning; }) => { const [rateLimits] = useAtom(rateLimitsAtom); @@ -151,11 +153,7 @@ export const TTDChatPanel = ({ onInsertMessage={onInsertMessage} onRetry={onRetry} rateLimits={rateLimits} - placeholder={{ - title: t("chat.placeholder.title"), - description: t("chat.placeholder.description"), - hint: t("chat.placeholder.hint"), - }} + renderWelcomeScreen={renderWelcomeScreen} renderWarning={renderWarning} /> diff --git a/packages/excalidraw/components/TTDDialog/TTDDialog.tsx b/packages/excalidraw/components/TTDDialog/TTDDialog.tsx index a77167613e..a16c0cc385 100644 --- a/packages/excalidraw/components/TTDDialog/TTDDialog.tsx +++ b/packages/excalidraw/components/TTDDialog/TTDDialog.tsx @@ -15,6 +15,8 @@ import { TTDDialogTab } from "./TTDDialogTab"; import "./TTDDialog.scss"; +import { TTDWelcomeMessage } from "./TTDWelcomeMessage"; + import type { MermaidToExcalidrawLibProps, TTDPersistenceAdapter, @@ -25,6 +27,7 @@ export const TTDDialog = ( props: | { onTextSubmit: TTTDDialog.onTextSubmit; + renderWelcomeScreen?: TTTDDialog.renderWelcomeScreen; renderWarning?: TTTDDialog.renderWarning; persistenceAdapter: TTDPersistenceAdapter; } @@ -39,6 +42,8 @@ export const TTDDialog = ( return ; }; +TTDDialog.WelcomeMessage = TTDWelcomeMessage; + /** * Text to diagram (TTD) dialog */ @@ -54,6 +59,7 @@ const TTDDialogBase = withInternalFallback( onTextSubmit( props: TTTDDialog.OnTextSubmitProps, ): Promise; + renderWelcomeScreen?: TTTDDialog.renderWelcomeScreen; renderWarning?: TTTDDialog.renderWarning; persistenceAdapter: TTDPersistenceAdapter; } @@ -110,6 +116,7 @@ const TTDDialogBase = withInternalFallback( diff --git a/packages/excalidraw/components/TTDDialog/TTDWelcomeMessage.tsx b/packages/excalidraw/components/TTDDialog/TTDWelcomeMessage.tsx new file mode 100644 index 0000000000..ae213a8b60 --- /dev/null +++ b/packages/excalidraw/components/TTDDialog/TTDWelcomeMessage.tsx @@ -0,0 +1,11 @@ +import { t } from "../../i18n"; + +export const TTDWelcomeMessage = () => { + return ( +
+

{t("chat.placeholder.title")}

+

{t("chat.placeholder.description")}

+

{t("chat.placeholder.hint")}

+
+ ); +}; diff --git a/packages/excalidraw/components/TTDDialog/TextToDiagram.tsx b/packages/excalidraw/components/TTDDialog/TextToDiagram.tsx index e93775a76b..da547602aa 100644 --- a/packages/excalidraw/components/TTDDialog/TextToDiagram.tsx +++ b/packages/excalidraw/components/TTDDialog/TextToDiagram.tsx @@ -35,6 +35,7 @@ import type { const TextToDiagramContent = ({ mermaidToExcalidrawLib, onTextSubmit, + renderWelcomeScreen, renderWarning, persistenceAdapter, }: { @@ -42,6 +43,7 @@ const TextToDiagramContent = ({ onTextSubmit: ( props: TTTDDialog.OnTextSubmitProps, ) => Promise; + renderWelcomeScreen?: TTTDDialog.renderWelcomeScreen; renderWarning?: TTTDDialog.renderWarning; persistenceAdapter: TTDPersistenceAdapter; }) => { @@ -220,6 +222,7 @@ const TextToDiagramContent = ({ onRetry={handleRetry} onViewAsMermaid={onViewAsMermaid} renderWarning={renderWarning} + renderWelcomeScreen={renderWelcomeScreen} /> {showPreview && ( ; + renderWelcomeScreen?: TTTDDialog.renderWelcomeScreen; renderWarning?: TTTDDialog.renderWarning; persistenceAdapter: TTDPersistenceAdapter; }) => { @@ -251,6 +256,7 @@ export const TextToDiagram = ({ diff --git a/packages/excalidraw/components/TTDDialog/types.ts b/packages/excalidraw/components/TTDDialog/types.ts index d3c8e3061d..7f79894b92 100644 --- a/packages/excalidraw/components/TTDDialog/types.ts +++ b/packages/excalidraw/components/TTDDialog/types.ts @@ -116,4 +116,9 @@ export namespace TTTDDialog { export type renderWarning = ( chatMessage: TChat.ChatMessage, ) => React.ReactNode | undefined; + + export type renderWelcomeScreen = (props: { + /** null if not rate limit data currently available */ + rateLimits: RateLimits | null; + }) => React.ReactNode | undefined; } From 216afc36252b793d88777cf85b8f51028a00e8b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rk=20Tolm=C3=A1cs?= Date: Fri, 30 Jan 2026 22:20:46 +0100 Subject: [PATCH 07/32] fix: Coherent stats binding (#10718) Signed-off-by: Mark Tolmacs --- packages/element/src/binding.ts | 92 ++++++++++++++++++++++++++++++--- 1 file changed, 84 insertions(+), 8 deletions(-) diff --git a/packages/element/src/binding.ts b/packages/element/src/binding.ts index c29c870260..b1e36ff13f 100644 --- a/packages/element/src/binding.ts +++ b/packages/element/src/binding.ts @@ -35,6 +35,7 @@ import { import { getAllHoveredElementAtPoint, getHoveredElementForBinding, + hitElementItself, intersectElementWithLineSegment, isBindableElementInsideOtherBindable, isPointInElement, @@ -1161,6 +1162,68 @@ export const updateBoundElements = ( }); }; +const updateArrowBindings = ( + latestElement: ExcalidrawArrowElement, + startOrEnd: "startBinding" | "endBinding", + elementsMap: NonDeletedSceneElementsMap, + scene: Scene, + appState: AppState, +) => { + invariant( + !isElbowArrow(latestElement), + "Elbow arrows not supported for indirect updates", + ); + + const binding = latestElement[startOrEnd]; + const bindableElement = + binding && + (elementsMap.get(binding.elementId) as ExcalidrawBindableElement); + const point = LinearElementEditor.getPointAtIndexGlobalCoordinates( + latestElement, + startOrEnd === "startBinding" ? 0 : -1, + elementsMap, + ); + const hit = + bindableElement && + hitElementItself({ + element: bindableElement, + point, + elementsMap, + threshold: maxBindingDistance_simple(appState.zoom), + }); + const strategyName = startOrEnd === "startBinding" ? "start" : "end"; + unbindBindingElement(latestElement, strategyName, scene); + if (hit) { + const pointIdx = + startOrEnd === "startBinding" ? 0 : latestElement.points.length - 1; + const localPoint = latestElement.points[pointIdx]; + const strategy = + getBindingStrategyForDraggingBindingElementEndpoints_simple( + latestElement, + new Map([[pointIdx, { point: localPoint }]]), + point[0], + point[1], + elementsMap, + scene.getNonDeletedElements(), + appState, + ); + if ( + strategy[strategyName] && + strategy[strategyName].element?.id === bindableElement.id && + strategy[strategyName].mode + ) { + bindBindingElement( + latestElement, + bindableElement, + strategy[strategyName].mode, + strategyName, + scene, + strategy[strategyName].focusPoint, + ); + } + } +}; + export const updateBindings = ( latestElement: ExcalidrawElement, scene: Scene, @@ -1171,14 +1234,27 @@ export const updateBindings = ( }, ) => { if (isArrowElement(latestElement)) { - bindOrUnbindBindingElement( - latestElement, - new Map(), - Infinity, - Infinity, - scene, - appState, - ); + const elementsMap = scene.getNonDeletedElementsMap(); + + if (latestElement.startBinding) { + updateArrowBindings( + latestElement, + "startBinding", + elementsMap, + scene, + appState, + ); + } + + if (latestElement.endBinding) { + updateArrowBindings( + latestElement, + "endBinding", + elementsMap, + scene, + appState, + ); + } } else { updateBoundElements(latestElement, scene, { ...options, From b552c6071417d731874c43e38d41dc6f76409fc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rk=20Tolm=C3=A1cs?= Date: Sat, 31 Jan 2026 15:08:28 +0100 Subject: [PATCH 08/32] feat: Focus indicator (#10613) * feat: Focus indicator Signed-off-by: Mark Tolmacs * fix: Snapshot update Signed-off-by: Mark Tolmacs * feat: Move visualdebug to utils and introduce volume bindable volume visualization Signed-off-by: Mark Tolmacs * fix: Move visualdebug to elements Due to dep circles Signed-off-by: Mark Tolmacs * fix: Possible test timeout Signed-off-by: Mark Tolmacs * fix: Incorrect hit test point input Signed-off-by: Mark Tolmacs * feat: Add fallback when dragged outside of allowed area Signed-off-by: Mark Tolmacs * fix: Elbow arrows don't need focus point mgmt Signed-off-by: Mark Tolmacs * fix: End bound indirect fix Signed-off-by: Mark Tolmacs * fix: Show indicator when arrow endpoint dragging Signed-off-by: Mark Tolmacs * fix: Update bound arrow endpoint at mid-point drag Signed-off-by: Mark Tolmacs * chore: Refactor Signed-off-by: Mark Tolmacs * fix: Curve endpoint intersection Signed-off-by: Mark Tolmacs * fix: Outline focus point is reset on existing arrow drag Signed-off-by: Mark Tolmacs * fix: Tests Signed-off-by: Mark Tolmacs * chore: Fix lint Signed-off-by: Mark Tolmacs * feat: Dragging focus point off Signed-off-by: Mark Tolmacs * fix: Don't show the focus indicator when arrow endpoint is dragged Signed-off-by: Mark Tolmacs * fix: Drag area for focus handles Signed-off-by: Mark Tolmacs * fix: Focus point size unified Signed-off-by: Mark Tolmacs * fix: Size bump for focus knob Signed-off-by: Mark Tolmacs * feat: Cache hits and scene lookups Signed-off-by: Mark Tolmacs * chore: Remove debug Signed-off-by: Mark Tolmacs * fix: Consider hit threshold and inside override too Signed-off-by: Mark Tolmacs * fix: Other shape switching Signed-off-by: Mark Tolmacs * perf: Update tolerance params Signed-off-by: Mark Tolmacs * fix: Focus know line width Signed-off-by: Mark Tolmacs * fix: knob offset Signed-off-by: Mark Tolmacs * fix: Full overlap Signed-off-by: Mark Tolmacs * chore: Remove Map caching Signed-off-by: Mark Tolmacs * fix: incorrect threshold Signed-off-by: Mark Tolmacs * fix: threshold setting Signed-off-by: Mark Tolmacs * fix: Hit caching Signed-off-by: Mark Tolmacs * fix: cache override Signed-off-by: Mark Tolmacs * fix: Snapshots Signed-off-by: Mark Tolmacs * feat: Redesigned focus point handling Signed-off-by: Mark Tolmacs * fix: Inside-inside mode Signed-off-by: Mark Tolmacs * chore: Remove comment Signed-off-by: Mark Tolmacs * feat: Allow focus knob outside the shape Signed-off-by: Mark Tolmacs * fix: Arrow endpoint offset Signed-off-by: Mark Tolmacs * fix: Focus knob element distance Signed-off-by: Mark Tolmacs * fix: Increase iteration on curve intersection calc Signed-off-by: Mark Tolmacs * fix: Handle disabled binding Signed-off-by: Mark Tolmacs * fix: Alt mode Signed-off-by: Mark Tolmacs * fix: Nested shape focus rewrite Signed-off-by: Mark Tolmacs * fix: Alt + Ctrl + arrow endpoitn Signed-off-by: Mark Tolmacs * fix: Hit ordering for focus points Signed-off-by: Mark Tolmacs * fix: Focus point visibility Signed-off-by: Mark Tolmacs * dry out renderFocusPointIndicator * do not higlight point when dragging & make focus point smaller * optimize retrieval of selectedLinearElement * move focus highlighting into renderFocusPointIndicator to DRY out and colocate * remove `disabled` state from focus highlight * make focus point stroke color less prominent * fix: No focus point for multi-point arrows Signed-off-by: Mark Tolmacs * fix: Arrow edit mode drag focus point release Signed-off-by: Mark Tolmacs * DRY out arrow point-like drag * move `focus.ts` to `arrows/focus.ts` --------- Signed-off-by: Mark Tolmacs Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com> --- packages/element/src/arrows/focus.ts | 541 ++++++++++++++++++ packages/element/src/arrows/helpers.ts | 45 ++ packages/element/src/binding.ts | 35 +- packages/element/src/collision.ts | 61 +- packages/element/src/elbowArrow.ts | 2 +- packages/element/src/index.ts | 2 + packages/element/src/linearElementEditor.ts | 88 ++- packages/element/src/zindex.ts | 9 +- .../tests/linearElementEditor.test.tsx | 18 +- packages/excalidraw/components/App.tsx | 152 ++++- .../components/Stats/stats.test.tsx | 3 +- .../excalidraw/renderer/interactiveScene.ts | 210 ++++++- .../tests/__snapshots__/history.test.tsx.snap | 24 +- .../regressionTests.test.tsx.snap | 8 + packages/excalidraw/tests/history.test.tsx | 2 +- packages/excalidraw/types.ts | 2 + 16 files changed, 1112 insertions(+), 90 deletions(-) create mode 100644 packages/element/src/arrows/focus.ts create mode 100644 packages/element/src/arrows/helpers.ts diff --git a/packages/element/src/arrows/focus.ts b/packages/element/src/arrows/focus.ts new file mode 100644 index 0000000000..960524465c --- /dev/null +++ b/packages/element/src/arrows/focus.ts @@ -0,0 +1,541 @@ +import { pointDistance, pointFrom, type GlobalPoint } from "@excalidraw/math"; +import { invariant } from "@excalidraw/common"; + +import type { AppState, NullableGridSize } from "@excalidraw/excalidraw/types"; + +import { + bindBindingElement, + calculateFixedPointForNonElbowArrowBinding, + FOCUS_POINT_SIZE, + getBindingGap, + getGlobalFixedPointForBindableElement, + isBindingEnabled, + maxBindingDistance_simple, + unbindBindingElement, + updateBoundPoint, +} from "../binding"; +import { + isBindableElement, + isBindingElement, + isElbowArrow, +} from "../typeChecks"; +import { LinearElementEditor } from "../linearElementEditor"; +import { getHoveredElementForFocusPoint, hitElementItself } from "../collision"; +import { moveArrowAboveBindable } from "../zindex"; + +import type { + ElementsMap, + ExcalidrawArrowElement, + ExcalidrawBindableElement, + NonDeletedSceneElementsMap, + PointsPositionUpdates, +} from "../types"; + +import type { Scene } from "../Scene"; + +export const isFocusPointVisible = ( + focusPoint: GlobalPoint, + arrow: ExcalidrawArrowElement, + bindableElement: ExcalidrawBindableElement, + elementsMap: ElementsMap, + appState: { + isBindingEnabled: AppState["isBindingEnabled"]; + zoom: AppState["zoom"]; + }, + ignoreOverlap = false, +): boolean => { + // No focus point management for elbow arrows, because elbow arrows + // always have their focus point at the arrow point itself + if ( + isElbowArrow(arrow) || + !isBindingEnabled(appState) || + arrow.points.length !== 2 + ) { + return false; + } + + // Avoid showing the focus point indicator if the focus point is essentially + // on top of the arrow point it belongs to itself, if not ignoring specifically + if (!ignoreOverlap) { + const associatedPointIdx = + arrow.startBinding?.elementId === bindableElement.id + ? 0 + : arrow.points.length - 1; + const associatedArrowPoint = + LinearElementEditor.getPointAtIndexGlobalCoordinates( + arrow, + associatedPointIdx, + elementsMap, + ); + + if ( + pointDistance(focusPoint, associatedArrowPoint) < + (FOCUS_POINT_SIZE * 1.5) / appState.zoom.value + ) { + return false; + } + } + + // Check if the focus point is within the element's shape bounds + return hitElementItself({ + element: bindableElement, + elementsMap, + point: focusPoint, + threshold: getBindingGap(bindableElement, arrow), + overrideShouldTestInside: true, + }); +}; + +// Updates the arrow endpoints in "orbit" configuration +const focusPointUpdate = ( + arrow: ExcalidrawArrowElement, + bindableElement: ExcalidrawBindableElement | null, + isStartBinding: boolean, + elementsMap: NonDeletedSceneElementsMap, + scene: Scene, + appState: AppState, + switchToInsideBinding: boolean, +) => { + const pointUpdates = new Map(); + + const bindingField = isStartBinding ? "startBinding" : "endBinding"; + const adjacentBindingField = isStartBinding ? "endBinding" : "startBinding"; + let currentBinding = arrow[bindingField]; + let adjacentBinding = arrow[adjacentBindingField]; + + // Update the dragged focus point related end + if (currentBinding && bindableElement) { + // Update the targeted bindings + const boundToSameElement = + bindableElement && + adjacentBinding && + currentBinding.elementId === adjacentBinding.elementId; + if (switchToInsideBinding || boundToSameElement) { + currentBinding = { + ...currentBinding, + mode: "inside", + }; + } else { + currentBinding = { + ...currentBinding, + mode: "orbit", + }; + } + + const pointIndex = isStartBinding ? 0 : arrow.points.length - 1; + const newPoint = updateBoundPoint( + arrow, + bindingField as "startBinding" | "endBinding", + currentBinding, + bindableElement, + elementsMap, + ); + + if (newPoint) { + pointUpdates.set(pointIndex, { point: newPoint }); + } + } + + // Also update the adjacent end if it has a binding + if (adjacentBinding) { + const adjacentBindableElement = elementsMap.get( + adjacentBinding.elementId, + ) as ExcalidrawBindableElement; + + if ( + adjacentBindableElement && + isBindableElement(adjacentBindableElement) && + isBindingEnabled(appState) + ) { + // Same shape bound on both ends + const boundToSameElementAfterUpdate = + bindableElement && adjacentBinding.elementId === bindableElement.id; + if (switchToInsideBinding || boundToSameElementAfterUpdate) { + adjacentBinding = { + ...adjacentBinding, + mode: "inside", + }; + } else { + adjacentBinding = { + ...adjacentBinding, + mode: "orbit", + }; + } + + const adjacentPointIndex = isStartBinding ? arrow.points.length - 1 : 0; + const adjacentNewPoint = updateBoundPoint( + arrow, + adjacentBindingField, + adjacentBinding, + adjacentBindableElement, + elementsMap, + ); + + if (adjacentNewPoint) { + pointUpdates.set(adjacentPointIndex, { + point: adjacentNewPoint, + }); + } + } + } + + if (pointUpdates.size > 0) { + LinearElementEditor.movePoints(arrow, scene, pointUpdates, { + [bindingField]: currentBinding, + [adjacentBindingField]: adjacentBinding, + }); + } +}; + +export const handleFocusPointDrag = ( + linearElementEditor: LinearElementEditor, + elementsMap: NonDeletedSceneElementsMap, + pointerCoords: { x: number; y: number }, + scene: Scene, + appState: AppState, + gridSize: NullableGridSize, + switchToInsideBinding: boolean, +) => { + const arrow = LinearElementEditor.getElement( + linearElementEditor.elementId, + elementsMap, + ) as any; + + // Sanity checks + if ( + !arrow || + !isBindingElement(arrow) || + isElbowArrow(arrow) || + !linearElementEditor.hoveredFocusPointBinding || + !linearElementEditor.draggedFocusPointBinding + ) { + return; + } + + const isStartBinding = + linearElementEditor.draggedFocusPointBinding === "start"; + const binding = isStartBinding ? arrow.startBinding : arrow.endBinding; + const { x: offsetX, y: offsetY } = linearElementEditor.pointerOffset; + const point = pointFrom( + pointerCoords.x - offsetX, + pointerCoords.y - offsetY, + ); + const bindingField = isStartBinding ? "startBinding" : "endBinding"; + const hit = getHoveredElementForFocusPoint( + point, + arrow, + scene.getNonDeletedElements(), + elementsMap, + maxBindingDistance_simple(appState.zoom), + ); + + // Hovering a bindable element + if (hit && isBindingEnabled(appState)) { + // Break existing binding if bound to another shape or if binding is disabled + if (arrow[bindingField] && hit.id !== binding?.elementId) { + unbindBindingElement( + arrow, + linearElementEditor.draggedFocusPointBinding, + scene, + ); + } + + // Handle binding mode switch + const newMode = + switchToInsideBinding && arrow[bindingField]?.mode === "orbit" + ? "inside" + : !switchToInsideBinding && arrow[bindingField]?.mode === "inside" + ? "orbit" + : null; + + // If no existing binding, create it + if (!arrow[bindingField] || newMode) { + // Create a new binding if none exists + bindBindingElement( + arrow, + hit, + newMode || "orbit", + linearElementEditor.draggedFocusPointBinding, + scene, + point, + ); + } + + // Update the binding's fixed point + scene.mutateElement(arrow, { + [bindingField]: { + ...arrow[bindingField], + elementId: hit.id, + mode: newMode || arrow[bindingField]?.mode || "orbit", + ...calculateFixedPointForNonElbowArrowBinding( + arrow, + hit, + linearElementEditor.draggedFocusPointBinding, + elementsMap, + point, + ), + }, + }); + } else { + // Not hovering any bindable element, move the arrow endpoint + const pointUpdates: PointsPositionUpdates = new Map(); + const pointIndex = isStartBinding ? 0 : arrow.points.length - 1; + pointUpdates.set(pointIndex, { + point: LinearElementEditor.createPointAt( + arrow, + elementsMap, + point[0], + point[1], + gridSize, + ), + }); + LinearElementEditor.movePoints(arrow, scene, pointUpdates); + if (arrow[bindingField]) { + unbindBindingElement(arrow, isStartBinding ? "start" : "end", scene); + } + } + + // Update the arrow endpoints + focusPointUpdate( + arrow, + hit, + isStartBinding, + elementsMap, + scene, + appState, + switchToInsideBinding, + ); + + if (hit && isBindingEnabled(appState)) { + moveArrowAboveBindable( + point, + arrow, + scene.getElementsIncludingDeleted(), + elementsMap, + scene, + hit, + ); + } +}; + +export const handleFocusPointPointerDown = ( + arrow: ExcalidrawArrowElement, + pointerDownState: { origin: { x: number; y: number } }, + elementsMap: NonDeletedSceneElementsMap, + appState: AppState, +): { + hitFocusPoint: "start" | "end" | null; + pointerOffset: { x: number; y: number }; +} => { + const pointerPos = pointFrom( + pointerDownState.origin.x, + pointerDownState.origin.y, + ); + const hitThreshold = (FOCUS_POINT_SIZE * 1.5) / appState.zoom.value; + + // Check start binding focus point + if (arrow.startBinding?.elementId) { + const bindableElement = elementsMap.get(arrow.startBinding.elementId); + if ( + bindableElement && + isBindableElement(bindableElement) && + !bindableElement.isDeleted + ) { + const focusPoint = getGlobalFixedPointForBindableElement( + arrow.startBinding.fixedPoint, + bindableElement, + elementsMap, + ); + if ( + isFocusPointVisible( + focusPoint, + arrow, + bindableElement, + elementsMap, + appState, + ) && + pointDistance(pointerPos, focusPoint) <= hitThreshold + ) { + return { + hitFocusPoint: "start", + pointerOffset: { + x: pointerPos[0] - focusPoint[0], + y: pointerPos[1] - focusPoint[1], + }, + }; + } + } + } + + // Check end binding focus point (only if start not already hit) + if (arrow.endBinding?.elementId) { + const bindableElement = elementsMap.get(arrow.endBinding.elementId); + if ( + bindableElement && + isBindableElement(bindableElement) && + !bindableElement.isDeleted + ) { + const focusPoint = getGlobalFixedPointForBindableElement( + arrow.endBinding.fixedPoint, + bindableElement, + elementsMap, + ); + if ( + isFocusPointVisible( + focusPoint, + arrow, + bindableElement, + elementsMap, + appState, + ) && + pointDistance(pointerPos, focusPoint) <= hitThreshold + ) { + return { + hitFocusPoint: "end", + pointerOffset: { + x: pointerPos[0] - focusPoint[0], + y: pointerPos[1] - focusPoint[1], + }, + }; + } + } + } + + return { + hitFocusPoint: null, + pointerOffset: { x: 0, y: 0 }, + }; +}; + +export const handleFocusPointPointerUp = ( + linearElementEditor: LinearElementEditor, + scene: Scene, +) => { + invariant( + linearElementEditor.draggedFocusPointBinding, + "Must have a dragged focus point at pointer release", + ); + + const arrow = LinearElementEditor.getElement( + linearElementEditor.elementId, + scene.getNonDeletedElementsMap(), + ); + invariant(arrow, "Arrow must be in the scene"); + + // Clean up + const bindingKey = + linearElementEditor.draggedFocusPointBinding === "start" + ? "startBinding" + : "endBinding"; + const otherBindingKey = + linearElementEditor.draggedFocusPointBinding === "start" + ? "endBinding" + : "startBinding"; + const boundElementId = arrow[bindingKey]?.elementId; + const otherBoundElementId = arrow[otherBindingKey]?.elementId; + const oldBoundElement = + boundElementId && + scene + .getNonDeletedElements() + .find( + (element) => + element.id !== boundElementId && + element.id !== otherBoundElementId && + isBindableElement(element) && + element.boundElements?.find(({ id }) => id === arrow.id), + ); + if (oldBoundElement) { + scene.mutateElement(oldBoundElement, { + boundElements: oldBoundElement.boundElements?.filter( + ({ id }) => id !== arrow.id, + ), + }); + } + + // Record the new bound element + const boundElement = + boundElementId && scene.getNonDeletedElementsMap().get(boundElementId); + if (boundElement) { + scene.mutateElement(boundElement, { + boundElements: [ + ...(boundElement.boundElements || [])?.filter( + ({ id }) => id !== arrow.id, + ), + { + id: arrow.id, + type: "arrow", + }, + ], + }); + } +}; + +export const handleFocusPointHover = ( + arrow: ExcalidrawArrowElement, + scenePointerX: number, + scenePointerY: number, + scene: Scene, + appState: AppState, +): "start" | "end" | null => { + const elementsMap = scene.getNonDeletedElementsMap(); + const pointerPos = pointFrom(scenePointerX, scenePointerY); + const hitThreshold = (FOCUS_POINT_SIZE * 1.5) / appState.zoom.value; + + // Check start binding focus point + if (arrow.startBinding?.elementId) { + const bindableElement = elementsMap.get(arrow.startBinding.elementId); + if ( + bindableElement && + isBindableElement(bindableElement) && + !bindableElement.isDeleted + ) { + const focusPoint = getGlobalFixedPointForBindableElement( + arrow.startBinding.fixedPoint, + bindableElement, + elementsMap, + ); + if ( + isFocusPointVisible( + focusPoint, + arrow, + bindableElement, + elementsMap, + appState, + ) && + pointDistance(pointerPos, focusPoint) <= hitThreshold + ) { + return "start"; + } + } + } + + // Check end binding focus point (only if start not already hovered) + if (arrow.endBinding?.elementId) { + const bindableElement = elementsMap.get(arrow.endBinding.elementId); + if ( + bindableElement && + isBindableElement(bindableElement) && + !bindableElement.isDeleted + ) { + const focusPoint = getGlobalFixedPointForBindableElement( + arrow.endBinding.fixedPoint, + bindableElement, + elementsMap, + ); + if ( + isFocusPointVisible( + focusPoint, + arrow, + bindableElement, + elementsMap, + appState, + ) && + pointDistance(pointerPos, focusPoint) <= hitThreshold + ) { + return "end"; + } + } + } + + return null; +}; diff --git a/packages/element/src/arrows/helpers.ts b/packages/element/src/arrows/helpers.ts new file mode 100644 index 0000000000..9f7c4ae9b3 --- /dev/null +++ b/packages/element/src/arrows/helpers.ts @@ -0,0 +1,45 @@ +import type { App } from "@excalidraw/excalidraw/types"; + +import { LinearElementEditor } from "../linearElementEditor"; + +import { handleFocusPointDrag } from "./focus"; + +export const maybeHandleArrowPointlikeDrag = ({ + app, + event, +}: { + app: App; + event: KeyboardEvent | React.KeyboardEvent | PointerEvent; +}): boolean => { + const appState = app.state; + if (appState.selectedLinearElement && app.lastPointerMoveCoords) { + // Update focus point status if the binding mode is changing + if (appState.selectedLinearElement.draggedFocusPointBinding) { + handleFocusPointDrag( + appState.selectedLinearElement, + app.scene.getNonDeletedElementsMap(), + app.lastPointerMoveCoords, + app.scene, + appState, + app.getEffectiveGridSize(), + event.altKey, + ); + return true; + } else if ( + appState.selectedLinearElement.hoverPointIndex !== null && + app.lastPointerMoveEvent && + appState.selectedLinearElement.initialState.lastClickedPoint >= 0 && + appState.selectedLinearElement.isDragging + ) { + LinearElementEditor.handlePointDragging( + app.lastPointerMoveEvent, + app, + app.lastPointerMoveCoords.x, + app.lastPointerMoveCoords.y, + appState.selectedLinearElement, + ); + return true; + } + } + return false; +}; diff --git a/packages/element/src/binding.ts b/packages/element/src/binding.ts index b1e36ff13f..074fe2a21f 100644 --- a/packages/element/src/binding.ts +++ b/packages/element/src/binding.ts @@ -116,6 +116,7 @@ export type BindingStrategy = */ export const BASE_BINDING_GAP = 10; export const BASE_BINDING_GAP_ELBOW = 5; +export const FOCUS_POINT_SIZE = 10 / 1.5; export const getBindingGap = ( bindTarget: ExcalidrawBindableElement, @@ -145,7 +146,9 @@ export const shouldEnableBindingForPointerEvent = ( return !event[KEYS.CTRL_OR_CMD]; }; -export const isBindingEnabled = (appState: AppState): boolean => { +export const isBindingEnabled = (appState: { + isBindingEnabled: AppState["isBindingEnabled"]; +}): boolean => { return appState.isBindingEnabled; }; @@ -259,7 +262,7 @@ const bindingStrategyForElbowArrowEndpointDragging = ( globalPoint, elements, elementsMap, - (element) => maxBindingDistance_simple(zoom), + maxBindingDistance_simple(zoom), ); const current = hit @@ -684,7 +687,7 @@ const getBindingStrategyForDraggingBindingElementEndpoints_simple = ( globalPoint, elements, elementsMap, - (e) => maxBindingDistance_simple(appState.zoom), + maxBindingDistance_simple(appState.zoom), ); const pointInElement = hit && @@ -711,7 +714,13 @@ const getBindingStrategyForDraggingBindingElementEndpoints_simple = ( const otherFocusPointIsInElement = otherBindableElement && otherFocusPoint && - isPointInElement(otherFocusPoint, otherBindableElement, elementsMap); + hitElementItself({ + point: otherFocusPoint, + element: otherBindableElement, + elementsMap, + threshold: 0, + overrideShouldTestInside: true, + }); // Handle outside-outside binding to the same element if (otherBinding && otherBinding.elementId === hit?.id) { @@ -1674,7 +1683,9 @@ export const updateBoundPoint = ( binding: FixedPointBinding | null | undefined, bindableElement: ExcalidrawBindableElement, elementsMap: ElementsMap, - customIntersector?: LineSegment, + opts?: { + customIntersector?: LineSegment; + }, ): LocalPoint | null => { if ( binding == null || @@ -1761,16 +1772,14 @@ export const updateBoundPoint = ( const isNested = (arrowTooShort || isOverlapping) && isLargerThanOther; - let _customIntersector = customIntersector; + let _customIntersector = opts?.customIntersector; if (!elbowed && !_customIntersector) { const [x1, y1, x2, y2] = LinearElementEditor.getElementAbsoluteCoords( arrow, elementsMap, ); const center = pointFrom((x1 + x2) / 2, (y1 + y2) / 2); - const edgePoint = isRectanguloidElement(bindableElement) - ? avoidRectangularCorner(arrow, bindableElement, elementsMap, global) - : global; + const edgePoint = global; const adjacentPoint = pointRotateRads( pointFrom( arrow.x + @@ -1884,7 +1893,7 @@ export const calculateFixedPointForNonElbowArrowBinding = ( elementsMap: ElementsMap, focusPoint?: GlobalPoint, ): { fixedPoint: FixedPoint } => { - const edgePoint = focusPoint + const edgePoint: GlobalPoint = focusPoint ? focusPoint : LinearElementEditor.getPointAtIndexGlobalCoordinates( linearElement, @@ -1892,11 +1901,7 @@ export const calculateFixedPointForNonElbowArrowBinding = ( elementsMap, ); - // Convert the global point to element-local coordinates - const elementCenter = pointFrom( - hoveredElement.x + hoveredElement.width / 2, - hoveredElement.y + hoveredElement.height / 2, - ); + const elementCenter = elementCenterPoint(hoveredElement, elementsMap); // Rotate the point to account for element rotation const nonRotatedPoint = pointRotateRads( diff --git a/packages/element/src/collision.ts b/packages/element/src/collision.ts index d93363a4d8..46d2dbd46a 100644 --- a/packages/element/src/collision.ts +++ b/packages/element/src/collision.ts @@ -59,8 +59,11 @@ import { LinearElementEditor } from "./linearElementEditor"; import { distanceToElement } from "./distance"; +import { getBindingGap } from "./binding"; + import type { ElementsMap, + ExcalidrawArrowElement, ExcalidrawBindableElement, ExcalidrawDiamondElement, ExcalidrawElement, @@ -290,7 +293,7 @@ export const getAllHoveredElementAtPoint = ( point: Readonly, elements: readonly Ordered[], elementsMap: NonDeletedSceneElementsMap, - toleranceFn?: (element: ExcalidrawBindableElement) => number, + tolerance?: number, ): NonDeleted[] => { const candidateElements: NonDeleted[] = []; // We need to to hit testing from front (end of the array) to back (beginning of the array) @@ -306,7 +309,7 @@ export const getAllHoveredElementAtPoint = ( if ( isBindableElement(element, false) && - bindingBorderTest(element, point, elementsMap, toleranceFn?.(element)) + bindingBorderTest(element, point, elementsMap, tolerance) ) { candidateElements.push(element); @@ -323,13 +326,13 @@ export const getHoveredElementForBinding = ( point: Readonly, elements: readonly Ordered[], elementsMap: NonDeletedSceneElementsMap, - toleranceFn?: (element: ExcalidrawBindableElement) => number, + tolerance?: number, ): NonDeleted | null => { const candidateElements = getAllHoveredElementAtPoint( point, elements, elementsMap, - toleranceFn, + tolerance, ); if (!candidateElements || candidateElements.length === 0) { @@ -348,6 +351,56 @@ export const getHoveredElementForBinding = ( .pop() as NonDeleted; }; +export const getHoveredElementForFocusPoint = ( + point: GlobalPoint, + arrow: ExcalidrawArrowElement, + elements: readonly Ordered[], + elementsMap: NonDeletedSceneElementsMap, + tolerance?: number, +): ExcalidrawBindableElement | null => { + const candidateElements: NonDeleted[] = []; + // We need to to hit testing from front (end of the array) to back (beginning of the array) + // because array is ordered from lower z-index to highest and we want element z-index + // with higher z-index + for (let index = elements.length - 1; index >= 0; --index) { + const element = elements[index]; + + invariant( + !element.isDeleted, + "Elements in the function parameter for getAllElementsAtPositionForBinding() should not contain deleted elements", + ); + + if ( + isBindableElement(element, false) && + bindingBorderTest(element, point, elementsMap, tolerance) + ) { + candidateElements.push(element); + } + } + + if (!candidateElements || candidateElements.length === 0) { + return null; + } + + if (candidateElements.length === 1) { + return candidateElements[0]; + } + + const distanceFilteredCandidateElements = candidateElements + // Resolve by distance + .filter( + (el) => + distanceToElement(el, elementsMap, point) <= getBindingGap(el, arrow) || + isPointInElement(point, el, elementsMap), + ); + + if (distanceFilteredCandidateElements.length === 0) { + return null; + } + + return distanceFilteredCandidateElements[0] as NonDeleted; +}; + /** * Intersect a line with an element for binding test * diff --git a/packages/element/src/elbowArrow.ts b/packages/element/src/elbowArrow.ts index b0a5e935f5..63b3b7926d 100644 --- a/packages/element/src/elbowArrow.ts +++ b/packages/element/src/elbowArrow.ts @@ -2276,7 +2276,7 @@ const getHoveredElement = ( origPoint, elements, elementsMap, - (element) => maxBindingDistance_simple(zoom), + maxBindingDistance_simple(zoom), ); }; diff --git a/packages/element/src/index.ts b/packages/element/src/index.ts index 381cc733bf..1ca1c1a289 100644 --- a/packages/element/src/index.ts +++ b/packages/element/src/index.ts @@ -70,6 +70,7 @@ export * from "./elbowArrow"; export * from "./elementLink"; export * from "./embeddable"; export * from "./flowchart"; +export * from "./arrows/focus"; export * from "./fractionalIndex"; export * from "./frame"; export * from "./groups"; @@ -97,3 +98,4 @@ export * from "./transformHandles"; export * from "./typeChecks"; export * from "./utils"; export * from "./zindex"; +export * from "./arrows/helpers"; diff --git a/packages/element/src/linearElementEditor.ts b/packages/element/src/linearElementEditor.ts index 00bf516350..125ef05025 100644 --- a/packages/element/src/linearElementEditor.ts +++ b/packages/element/src/linearElementEditor.ts @@ -149,6 +149,8 @@ export class LinearElementEditor { public readonly pointerOffset: Readonly<{ x: number; y: number }>; public readonly hoverPointIndex: number; public readonly segmentMidPointHoveredCoords: GlobalPoint | null; + public readonly hoveredFocusPointBinding: "start" | "end" | null; + public readonly draggedFocusPointBinding: "start" | "end" | null; public readonly elbowed: boolean; public readonly customLineAngle: number | null; public readonly isEditing: boolean; @@ -194,6 +196,8 @@ export class LinearElementEditor { }; this.hoverPointIndex = -1; this.segmentMidPointHoveredCoords = null; + this.hoveredFocusPointBinding = null; + this.draggedFocusPointBinding = null; this.elbowed = isElbowArrow(element) && element.elbowed; this.customLineAngle = null; this.isEditing = isEditing; @@ -2135,6 +2139,63 @@ const pointDraggingUpdates = ( }; } + // Handle the case where neither endpoint is being dragged + // but we need to update bound endpoints + if (!startIsDragged && !endIsDragged) { + const nextArrow = { + ...element, + points: element.points.map((p, idx) => { + return naiveDraggingPoints.get(idx)?.point ?? p; + }), + }; + const positions = new Map(naiveDraggingPoints); + + if (element.startBinding) { + const startBindable = elementsMap.get(element.startBinding.elementId) as + | ExcalidrawBindableElement + | undefined; + if (startBindable) { + const startPoint = + updateBoundPoint( + nextArrow, + "startBinding", + element.startBinding, + startBindable, + elementsMap, + ) ?? null; + if (startPoint) { + positions.set(0, { point: startPoint, isDragging: true }); + } + } + } + + if (element.endBinding) { + const endBindable = elementsMap.get(element.endBinding.elementId) as + | ExcalidrawBindableElement + | undefined; + if (endBindable) { + const endPoint = + updateBoundPoint( + nextArrow, + "endBinding", + element.endBinding, + endBindable, + elementsMap, + ) ?? null; + if (endPoint) { + positions.set(element.points.length - 1, { + point: endPoint, + isDragging: true, + }); + } + } + } + + return { + positions, + }; + } + if (startIsDragged === endIsDragged) { return { positions: naiveDraggingPoints, @@ -2279,7 +2340,9 @@ const pointDraggingUpdates = ( nextArrow.endBinding, endBindable, elementsMap, - endCustomIntersector, + { + customIntersector: endCustomIntersector, + }, ) || nextArrow.points[nextArrow.points.length - 1] : nextArrow.points[nextArrow.points.length - 1]; @@ -2302,7 +2365,7 @@ const pointDraggingUpdates = ( : startIsDraggingOverEndElement && app.state.bindMode !== "inside" && getFeatureFlag("COMPLEX_BINDINGS") - ? nextArrow.points[nextArrow.points.length - 1] + ? endLocalPoint : startBindable ? updateBoundPoint( element, @@ -2310,15 +2373,18 @@ const pointDraggingUpdates = ( nextArrow.startBinding, startBindable, elementsMap, - startCustomIntersector, + { customIntersector: startCustomIntersector }, ) || nextArrow.points[0] : nextArrow.points[0]; const endChanged = - pointDistance( - endLocalPoint, - nextArrow.points[nextArrow.points.length - 1], - ) !== 0; + !startIsDraggingOverEndElement && + !( + endIsDraggingOverStartElement && + app.state.bindMode !== "inside" && + getFeatureFlag("COMPLEX_BINDINGS") + ) && + !!endBindable; const startChanged = pointDistance(startLocalPoint, nextArrow.points[0]) !== 0; @@ -2332,13 +2398,7 @@ const pointDraggingUpdates = ( const indices = Array.from(indicesSet); return { - updates: - updates.startBinding || updates.suggestedBinding - ? { - startBinding: updates.startBinding, - suggestedBinding: updates.suggestedBinding, - } - : undefined, + updates, positions: new Map( indices.map((idx) => { return [ diff --git a/packages/element/src/zindex.ts b/packages/element/src/zindex.ts index 0bb0cda9c2..c4171c6bbd 100644 --- a/packages/element/src/zindex.ts +++ b/packages/element/src/zindex.ts @@ -156,12 +156,11 @@ export const moveArrowAboveBindable = ( elements: readonly Ordered[], elementsMap: NonDeletedSceneElementsMap, scene: Scene, + hit?: NonDeletedExcalidrawElement, ): readonly OrderedExcalidrawElement[] => { - const hoveredElement = getHoveredElementForBinding( - point, - elements, - elementsMap, - ); + const hoveredElement = hit + ? hit + : getHoveredElementForBinding(point, elements, elementsMap); if (!hoveredElement) { return elements; diff --git a/packages/element/tests/linearElementEditor.test.tsx b/packages/element/tests/linearElementEditor.test.tsx index 5b31892b4d..4c9ab3825d 100644 --- a/packages/element/tests/linearElementEditor.test.tsx +++ b/packages/element/tests/linearElementEditor.test.tsx @@ -378,7 +378,7 @@ describe("Test Linear Elements", () => { // drag line from midpoint drag(midpoint, pointFrom(midpoint[0] + delta, midpoint[1] + delta)); expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( - `11`, + `12`, ); expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`); @@ -419,7 +419,7 @@ describe("Test Linear Elements", () => { fireEvent.click(screen.getByTitle("Round")); expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( - `9`, + `10`, ); expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`); @@ -480,7 +480,7 @@ describe("Test Linear Elements", () => { drag(startPoint, endPoint); expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( - `11`, + `12`, ); expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`); @@ -548,7 +548,7 @@ describe("Test Linear Elements", () => { ); expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( - `14`, + `15`, ); expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`9`); @@ -599,7 +599,7 @@ describe("Test Linear Elements", () => { drag(hitCoords, pointFrom(hitCoords[0] - delta, hitCoords[1] - delta)); expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( - `11`, + `12`, ); expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`); @@ -640,7 +640,7 @@ describe("Test Linear Elements", () => { drag(hitCoords, pointFrom(hitCoords[0] + delta, hitCoords[1] + delta)); expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( - `11`, + `12`, ); expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`); @@ -688,7 +688,7 @@ describe("Test Linear Elements", () => { deletePoint(points[2]); expect(line.points.length).toEqual(3); expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( - `17`, + `18`, ); expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`10`); @@ -746,7 +746,7 @@ describe("Test Linear Elements", () => { ), ); expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( - `14`, + `15`, ); expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`9`); expect(line.points.length).toEqual(5); @@ -844,7 +844,7 @@ describe("Test Linear Elements", () => { drag(hitCoords, pointFrom(hitCoords[0] + delta, hitCoords[1] + delta)); expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( - `11`, + `12`, ); expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`); diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 2c9f2f7308..e224ca97db 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -250,6 +250,11 @@ import { maxBindingDistance_simple, convertToExcalidrawElements, type ExcalidrawElementSkeleton, + handleFocusPointDrag, + handleFocusPointHover, + handleFocusPointPointerDown, + handleFocusPointPointerUp, + maybeHandleArrowPointlikeDrag, } from "@excalidraw/element"; import type { GlobalPoint, LocalPoint, Radians } from "@excalidraw/math"; @@ -4734,8 +4739,12 @@ class App extends React.Component { } // Handle Alt key for bind mode - if (event.key === KEYS.ALT && getFeatureFlag("COMPLEX_BINDINGS")) { - this.handleSkipBindMode(); + if (event.key === KEYS.ALT) { + if (getFeatureFlag("COMPLEX_BINDINGS")) { + this.handleSkipBindMode(); + } else { + maybeHandleArrowPointlikeDrag({ app: this, event }); + } } if (this.actionManager.handleKeyDown(event)) { @@ -4751,7 +4760,11 @@ class App extends React.Component { this.resetDelayedBindMode(); } - this.setState({ isBindingEnabled: false }); + flushSync(() => { + this.setState({ isBindingEnabled: false }); + }); + + maybeHandleArrowPointlikeDrag({ app: this, event }); } if (isArrowKey(event.key)) { @@ -5024,6 +5037,11 @@ class App extends React.Component { } isHoldingSpace = false; } + + if (event.key === KEYS.ALT) { + maybeHandleArrowPointlikeDrag({ app: this, event }); + } + if ( (event.key === KEYS.ALT && this.state.bindMode === "skip") || (!event[KEYS.CTRL_OR_CMD] && !isBindingEnabled(this.state)) @@ -5034,7 +5052,7 @@ class App extends React.Component { }); // Restart the timer if we're creating/editing a linear element and hovering over an element - if (this.lastPointerMoveEvent) { + if (this.lastPointerMoveEvent && getFeatureFlag("COMPLEX_BINDINGS")) { const scenePointer = viewportCoordsToSceneCoords( { clientX: this.lastPointerMoveEvent.clientX, @@ -5055,14 +5073,18 @@ class App extends React.Component { this.scene.getNonDeletedElementsMap(), ); - if (isBindingElement(element) && getFeatureFlag("COMPLEX_BINDINGS")) { + if (isBindingElement(element)) { this.handleDelayedBindModeChange(element, hoveredElement); } } } } if (!event[KEYS.CTRL_OR_CMD] && !this.state.isBindingEnabled) { - this.setState({ isBindingEnabled: true }); + flushSync(() => { + this.setState({ isBindingEnabled: true }); + }); + + maybeHandleArrowPointlikeDrag({ app: this, event }); } if (isArrowKey(event.key)) { bindOrUnbindBindingElements( @@ -6379,7 +6401,7 @@ class App extends React.Component { pointFrom(scenePointerX, scenePointerY), this.scene.getNonDeletedElements(), this.scene.getNonDeletedElementsMap(), - (el) => maxBindingDistance_simple(this.state.zoom), + maxBindingDistance_simple(this.state.zoom), ); if (hoveredElement) { this.setState({ @@ -6410,7 +6432,7 @@ class App extends React.Component { pointFrom(scenePointerX, scenePointerY), this.scene.getNonDeletedElements(), this.scene.getNonDeletedElementsMap(), - (el) => maxBindingDistance_simple(this.state.zoom), + maxBindingDistance_simple(this.state.zoom), ); if (hoveredElement) { this.actionManager.executeAction(actionFinalize, "ui", { @@ -6564,7 +6586,7 @@ class App extends React.Component { pointFrom(scenePointerX, scenePointerY), this.scene.getNonDeletedElements(), this.scene.getNonDeletedElementsMap(), - (el) => maxBindingDistance_simple(this.state.zoom), + maxBindingDistance_simple(this.state.zoom), ); if ( hit && @@ -6923,6 +6945,37 @@ class App extends React.Component { }, }); } + + // Check for focus point hover + let hoveredFocusPointBinding: "start" | "end" | null = null; + const arrow = element as any; + if (arrow.startBinding || arrow.endBinding) { + hoveredFocusPointBinding = handleFocusPointHover( + element as ExcalidrawArrowElement, + scenePointerX, + scenePointerY, + this.scene, + this.state, + ); + } + + if ( + this.state.selectedLinearElement.hoveredFocusPointBinding !== + hoveredFocusPointBinding + ) { + this.setState({ + selectedLinearElement: { + ...this.state.selectedLinearElement, + isDragging: false, + hoveredFocusPointBinding, + }, + }); + } + + // Set cursor to pointer when hovering over a focus point + if (hoveredFocusPointBinding) { + setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER); + } } else { setCursor(this.interactiveCanvas, CURSOR_TYPE.AUTO); } @@ -7844,6 +7897,37 @@ class App extends React.Component { if (ret.didAddPoint) { return true; } + + // Also check at current pointer position if focus point is being hovered + // (in case we're clicking directly without a prior move event) + const elementsMap = this.scene.getNonDeletedElementsMap(); + const arrow = LinearElementEditor.getElement( + linearElementEditor.elementId, + elementsMap, + ) as any; + + if (arrow && isBindingElement(arrow)) { + const { hitFocusPoint, pointerOffset } = + handleFocusPointPointerDown( + arrow, + pointerDownState, + elementsMap, + this.state, + ); + + // If focus point is hit, update state and prevent element selection + if (hitFocusPoint) { + this.setState({ + selectedLinearElement: { + ...linearElementEditor, + hoveredFocusPointBinding: hitFocusPoint, + draggedFocusPointBinding: hitFocusPoint, + pointerOffset, + }, + }); + return false; + } + } } const allHitElements = this.getElementsAtPosition( @@ -8991,6 +9075,31 @@ class App extends React.Component { if (this.state.selectedLinearElement) { const linearElementEditor = this.state.selectedLinearElement; + // Handle focus point dragging if needed + if (linearElementEditor.draggedFocusPointBinding) { + handleFocusPointDrag( + linearElementEditor, + elementsMap, + pointerCoords, + this.scene, + this.state, + this.getEffectiveGridSize(), + event.altKey, + ); + this.setState({ + selectedLinearElement: { + ...linearElementEditor, + isDragging: false, + selectedPointsIndices: [], + initialState: { + ...linearElementEditor.initialState, + lastClickedPoint: -1, + }, + }, + }); + return; + } + if ( LinearElementEditor.shouldAddMidpoint( this.state.selectedLinearElement, @@ -9859,12 +9968,14 @@ class App extends React.Component { // and sets binding element if ( this.state.selectedLinearElement?.isEditing && - !this.state.newElement + !this.state.newElement && + this.state.selectedLinearElement.draggedFocusPointBinding === null ) { if ( !pointerDownState.boxSelection.hasOccurred && pointerDownState.hit?.element?.id !== - this.state.selectedLinearElement.elementId + this.state.selectedLinearElement.elementId && + this.state.selectedLinearElement.draggedFocusPointBinding === null ) { this.actionManager.executeAction(actionFinalize); } else { @@ -9900,7 +10011,18 @@ class App extends React.Component { } } - if ( + if (this.state.selectedLinearElement.draggedFocusPointBinding) { + handleFocusPointPointerUp( + this.state.selectedLinearElement, + this.scene, + ); + this.setState({ + selectedLinearElement: { + ...this.state.selectedLinearElement, + draggedFocusPointBinding: null, + }, + }); + } else if ( pointerDownState.hit?.element?.id !== this.state.selectedLinearElement.elementId ) { @@ -9910,6 +10032,12 @@ class App extends React.Component { this.setState({ selectedLinearElement: null }); } } else if (this.state.selectedLinearElement.isDragging) { + this.setState({ + selectedLinearElement: { + ...this.state.selectedLinearElement, + isDragging: false, + }, + }); this.actionManager.executeAction(actionFinalize, "ui", { event: childEvent, sceneCoords, diff --git a/packages/excalidraw/components/Stats/stats.test.tsx b/packages/excalidraw/components/Stats/stats.test.tsx index 548a16ffa4..9ea376580b 100644 --- a/packages/excalidraw/components/Stats/stats.test.tsx +++ b/packages/excalidraw/components/Stats/stats.test.tsx @@ -135,7 +135,8 @@ describe("binding with linear elements", () => { ) as HTMLInputElement; expect(linear.startBinding).not.toBe(null); expect(inputX).not.toBeNull(); - UI.updateInput(inputX, String("186")); + + UI.updateInput(inputX, String("184")); expect(linear.startBinding).not.toBe(null); }); diff --git a/packages/excalidraw/renderer/interactiveScene.ts b/packages/excalidraw/renderer/interactiveScene.ts index 5f314c2557..0c55dfaac4 100644 --- a/packages/excalidraw/renderer/interactiveScene.ts +++ b/packages/excalidraw/renderer/interactiveScene.ts @@ -21,10 +21,13 @@ import { deconstructDiamondElement, deconstructRectanguloidElement, elementCenterPoint, + FOCUS_POINT_SIZE, getOmitSidesForEditorInterface, getTransformHandles, getTransformHandlesFromCoords, hasBoundingBox, + isArrowElement, + isBindableElement, isElbowArrow, isFrameLikeElement, isImageElement, @@ -44,6 +47,10 @@ import { } from "@excalidraw/element"; import { getCommonBounds, getElementAbsoluteCoords } from "@excalidraw/element"; +import { + getGlobalFixedPointForBindableElement, + isFocusPointVisible, +} from "@excalidraw/element"; import type { TransformHandles, @@ -52,6 +59,7 @@ import type { import type { ElementsMap, + ExcalidrawArrowElement, ExcalidrawBindableElement, ExcalidrawElement, ExcalidrawFrameLikeElement, @@ -71,11 +79,6 @@ import { SCROLLBAR_WIDTH, } from "../scene/scrollbars"; -import { - type AppClassProperties, - type InteractiveCanvasAppState, -} from "../types"; - import { getClientColor, renderRemoteCursors } from "../clients"; import { @@ -85,6 +88,8 @@ import { strokeRectWithRotation_simple, } from "./helpers"; +import type { AppClassProperties, InteractiveCanvasAppState } from "../types"; + import type { InteractiveCanvasRenderConfig, InteractiveSceneRenderConfig, @@ -123,6 +128,9 @@ const renderLinearElementPointHighlight = ( ) { return; } + if (appState.selectedLinearElement?.isDragging) { + return; + } const element = LinearElementEditor.getElement(elementId, elementsMap); if (!element) { @@ -156,6 +164,19 @@ const highlightPoint = ( ); }; +const renderFocusPointHighlight = ( + context: CanvasRenderingContext2D, + appState: InteractiveCanvasAppState, + focusPoint: GlobalPoint, +) => { + context.save(); + context.translate(appState.scrollX, appState.scrollY); + + highlightPoint(focusPoint, context, appState); + + context.restore(); +}; + const renderSingleLinearPoint = ( context: CanvasRenderingContext2D, appState: InteractiveCanvasAppState, @@ -920,6 +941,145 @@ const renderLinearPointHandles = ( context.restore(); }; +const renderFocusPointConnectionLine = ( + context: CanvasRenderingContext2D, + appState: InteractiveCanvasAppState, + fromPoint: GlobalPoint, + toPoint: GlobalPoint, +) => { + context.save(); + context.translate(appState.scrollX, appState.scrollY); + + context.strokeStyle = "rgba(134, 131, 226, 0.6)"; + context.lineWidth = 1 / appState.zoom.value; + context.setLineDash([4 / appState.zoom.value, 4 / appState.zoom.value]); + + context.beginPath(); + context.moveTo(fromPoint[0], fromPoint[1]); + context.lineTo(toPoint[0], toPoint[1]); + context.stroke(); + + context.restore(); +}; + +const renderFocusPointCicle = ( + context: CanvasRenderingContext2D, + appState: InteractiveCanvasAppState, + point: GlobalPoint, + radius: number, + isHovered: boolean, +) => { + context.save(); + context.translate(appState.scrollX, appState.scrollY); + context.strokeStyle = "rgba(134, 131, 226, 0.6)"; + context.lineWidth = 1 / appState.zoom.value; + context.setLineDash([]); + context.fillStyle = isHovered + ? "rgba(134, 131, 226, 0.9)" + : "rgba(255, 255, 255, 0.9)"; + + fillCircle( + context, + point[0], + point[1], + radius / appState.zoom.value, + true, + true, + ); + context.restore(); +}; + +const renderFocusPointIndicator = ({ + arrow, + appState, + type, + context, + elementsMap, +}: { + arrow: NonDeleted; + appState: InteractiveCanvasAppState; + context: CanvasRenderingContext2D; + elementsMap: NonDeletedSceneElementsMap; + type: "start" | "end"; +}) => { + const binding = type === "start" ? arrow.startBinding : arrow.endBinding; + const bindableElement = + binding?.elementId && elementsMap.get(binding.elementId); + + if ( + !bindableElement || + !isBindableElement(bindableElement) || + bindableElement.isDeleted + ) { + return; + } + + const focusPoint = getGlobalFixedPointForBindableElement( + binding.fixedPoint, + bindableElement, + elementsMap, + ); + + // Only render if focus point is within the bindable element + if ( + !isFocusPointVisible( + focusPoint, + arrow, + bindableElement, + elementsMap, + appState, + ) + ) { + return; + } + + const linearState = appState.selectedLinearElement; + const isDragging = !!linearState?.isDragging; + const pointIndex = type === "start" ? 0 : arrow.points.length - 1; + const pointSelected = + !!linearState?.selectedPointsIndices?.includes(pointIndex); + + // render focus point highlight + // ---------------------------- + + if ( + linearState?.hoveredFocusPointBinding === type && + !linearState.draggedFocusPointBinding + ) { + renderFocusPointHighlight(context, appState, focusPoint); + } + + // render focus point + // ---------------------------- + + if (!(pointSelected && isDragging)) { + const focusPoint = getGlobalFixedPointForBindableElement( + binding.fixedPoint, + bindableElement, + elementsMap, + ); + + const isHovered = linearState?.hoveredFocusPointBinding === type; + + // Render dashed line from arrow start point to focus point + const arrowPoint = LinearElementEditor.getPointAtIndexGlobalCoordinates( + arrow, + pointIndex, + elementsMap, + ); + + renderFocusPointConnectionLine(context, appState, arrowPoint, focusPoint); + + renderFocusPointCicle( + context, + appState, + focusPoint, + FOCUS_POINT_SIZE / 1.5, + isHovered, + ); + } +}; + const renderTransformHandles = ( context: CanvasRenderingContext2D, renderConfig: InteractiveCanvasRenderConfig, @@ -1251,26 +1411,44 @@ const _renderInteractiveScene = ({ ); } + const linearState = appState.selectedLinearElement; + const selectedLinearElement = + linearState && + LinearElementEditor.getElement(linearState.elementId, allElementsMap); // Arrows have a different highlight behavior when // they are the only selected element - if (appState.selectedLinearElement) { - const editor = appState.selectedLinearElement; - const firstSelectedLinear = selectedElements.find( - (el) => el.id === editor.elementId, // Don't forget bound text elements! - ); - + if (selectedLinearElement) { if (!appState.selectedLinearElement.isDragging) { - if (editor.segmentMidPointHoveredCoords) { + if (linearState.segmentMidPointHoveredCoords) { renderElbowArrowMidPointHighlight(context, appState); } else if ( - isElbowArrow(firstSelectedLinear) - ? editor.hoverPointIndex === 0 || - editor.hoverPointIndex === firstSelectedLinear.points.length - 1 - : editor.hoverPointIndex >= 0 + isElbowArrow(selectedLinearElement) + ? linearState.hoverPointIndex === 0 || + linearState.hoverPointIndex === + selectedLinearElement.points.length - 1 + : linearState.hoverPointIndex >= 0 ) { renderLinearElementPointHighlight(context, appState, elementsMap); } } + + if (isArrowElement(selectedLinearElement)) { + renderFocusPointIndicator({ + arrow: selectedLinearElement, + elementsMap: allElementsMap, + appState, + context, + type: "start", + }); + + renderFocusPointIndicator({ + arrow: selectedLinearElement, + elementsMap: allElementsMap, + appState, + context, + type: "end", + }); + } } // Paint selected elements diff --git a/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap index d87b67f8d7..4e9d7bb568 100644 --- a/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap @@ -2398,7 +2398,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": "353.93938", + "height": "390.11932", "id": "id4", "index": "a2", "isDeleted": false, @@ -2412,8 +2412,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl 0, ], [ - "478.03877", - "-353.93938", + "478.03878", + "-390.11932", ], ], "roughness": 1, @@ -2424,7 +2424,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "startBinding": { "elementId": "id0", "fixedPoint": [ - "0.50010", + 1, "0.50010", ], "mode": "orbit", @@ -2435,9 +2435,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "type": "arrow", "updated": 1, "version": 12, - "width": "478.03877", + "width": "478.03878", "x": 11, - "y": "-45.14705", + "y": "-8.96692", } `; @@ -2566,7 +2566,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": "353.93938", + "height": "390.11932", "index": "a2", "isDeleted": false, "link": null, @@ -2578,8 +2578,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl 0, ], [ - "478.03877", - "-353.93938", + "478.03878", + "-390.11932", ], ], "roughness": 1, @@ -2590,7 +2590,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "startBinding": { "elementId": "id0", "fixedPoint": [ - "0.50010", + 1, "0.50010", ], "mode": "orbit", @@ -2600,9 +2600,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "strokeWidth": 2, "type": "arrow", "version": 12, - "width": "478.03877", + "width": "478.03878", "x": 11, - "y": "-45.14705", + "y": "-8.96692", }, "inserted": { "isDeleted": true, diff --git a/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap index f5ab13c34f..d9f469988c 100644 --- a/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap @@ -8743,9 +8743,11 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1` "selectedGroupIds": {}, "selectedLinearElement": LinearElementEditor { "customLineAngle": null, + "draggedFocusPointBinding": null, "elbowed": false, "elementId": "id0", "hoverPointIndex": -1, + "hoveredFocusPointBinding": null, "initialState": { "altFocusPoint": null, "arrowStartIsInside": false, @@ -8976,9 +8978,11 @@ exports[`regression tests > key 6 selects line tool > [end of test] appState 1`] "selectedGroupIds": {}, "selectedLinearElement": LinearElementEditor { "customLineAngle": null, + "draggedFocusPointBinding": null, "elbowed": false, "elementId": "id0", "hoverPointIndex": -1, + "hoveredFocusPointBinding": null, "initialState": { "altFocusPoint": null, "arrowStartIsInside": false, @@ -9402,9 +9406,11 @@ exports[`regression tests > key a selects arrow tool > [end of test] appState 1` "selectedGroupIds": {}, "selectedLinearElement": LinearElementEditor { "customLineAngle": null, + "draggedFocusPointBinding": null, "elbowed": false, "elementId": "id0", "hoverPointIndex": -1, + "hoveredFocusPointBinding": null, "initialState": { "altFocusPoint": null, "arrowStartIsInside": false, @@ -9818,9 +9824,11 @@ exports[`regression tests > key l selects line tool > [end of test] appState 1`] "selectedGroupIds": {}, "selectedLinearElement": LinearElementEditor { "customLineAngle": null, + "draggedFocusPointBinding": null, "elbowed": false, "elementId": "id0", "hoverPointIndex": -1, + "hoveredFocusPointBinding": null, "initialState": { "altFocusPoint": null, "arrowStartIsInside": false, diff --git a/packages/excalidraw/tests/history.test.tsx b/packages/excalidraw/tests/history.test.tsx index d3b0cf48f3..014d8608ed 100644 --- a/packages/excalidraw/tests/history.test.tsx +++ b/packages/excalidraw/tests/history.test.tsx @@ -5051,7 +5051,7 @@ describe("history", () => { id: arrowId, startBinding: expect.objectContaining({ elementId: rect1.id, - fixedPoint: expect.arrayContaining([0.5001, 0.5001]), + fixedPoint: expect.arrayContaining([1, 0.5001]), }), endBinding: expect.objectContaining({ elementId: rect2.id, diff --git a/packages/excalidraw/types.ts b/packages/excalidraw/types.ts index 2f464d8683..eeb8aa8056 100644 --- a/packages/excalidraw/types.ts +++ b/packages/excalidraw/types.ts @@ -63,6 +63,8 @@ import type { isOverScrollBars } from "./scene/scrollbars"; import type React from "react"; import type { JSX } from "react"; +export type { App }; + export type SocketId = string & { _brand: "SocketId" }; export type Collaborator = Readonly<{ From dfa1ce572bef6976ae82b3c5cfaa581580a447f1 Mon Sep 17 00:00:00 2001 From: zsviczian Date: Sat, 31 Jan 2026 16:28:21 +0100 Subject: [PATCH 09/32] fix: SVG Inversion on Safari (#10712) * invert image on safari * lint * Inversion to match theme filter * cleanup * Adjust canvas dimensions for device pixel ratio when inverting on Safari * revert inversion algo & handle darkMode placeholder --------- Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com> --- packages/element/src/renderElement.ts | 94 +++++++++++++++++++++------ 1 file changed, 74 insertions(+), 20 deletions(-) diff --git a/packages/element/src/renderElement.ts b/packages/element/src/renderElement.ts index 1c5f941ebf..96bff999bc 100644 --- a/packages/element/src/renderElement.ts +++ b/packages/element/src/renderElement.ts @@ -23,6 +23,7 @@ import { getVerticalOffset, invariant, applyDarkModeFilter, + isSafari, } from "@excalidraw/common"; import type { @@ -360,8 +361,9 @@ IMAGE_ERROR_PLACEHOLDER_IMG.src = `data:${MIME_TYPES.svg},${encodeURIComponent( const drawImagePlaceholder = ( element: ExcalidrawImageElement, context: CanvasRenderingContext2D, + theme: StaticCanvasRenderConfig["theme"], ) => { - context.fillStyle = "#E7E7E7"; + context.fillStyle = theme === THEME.DARK ? "#2E2E2E" : "#E7E7E7"; context.fillRect(0, 0, element.width, element.height); const imageMinWidthOrHeight = Math.min(element.width, element.height); @@ -443,13 +445,6 @@ const drawElementOnCanvas = ( ? cacheEntry?.image : undefined; - const shouldInvertImage = - renderConfig.theme === THEME.DARK && - cacheEntry?.mimeType === MIME_TYPES.svg; - - if (shouldInvertImage) { - context.filter = DARK_THEME_FILTER; - } if (img != null && !(img instanceof Promise)) { if (element.roundness && context.roundRect) { context.beginPath(); @@ -472,19 +467,78 @@ const drawElementOnCanvas = ( height: img.naturalHeight, }; - context.drawImage( - img, - x, - y, - width, - height, - 0 /* hardcoded for the selection box*/, - 0, - element.width, - element.height, - ); + const shouldInvertImage = + renderConfig.theme === THEME.DARK && + cacheEntry?.mimeType === MIME_TYPES.svg; + + if (shouldInvertImage && isSafari) { + const devicePixelRatio = window.devicePixelRatio || 1; + const tempCanvas = document.createElement("canvas"); + tempCanvas.width = element.width * devicePixelRatio; + tempCanvas.height = element.height * devicePixelRatio; + const tempContext = tempCanvas.getContext("2d"); + + if (tempContext) { + tempContext.scale(devicePixelRatio, devicePixelRatio); + tempContext.drawImage( + img, + x, + y, + width, + height, + 0, + 0, + element.width, + element.height, + ); + + const imageData = tempContext.getImageData( + 0, + 0, + tempCanvas.width, + tempCanvas.height, + ); + + const data = imageData.data; + + for (let i = 0; i < data.length; i += 4) { + data[i] = 255 - data[i]; + data[i + 1] = 255 - data[i + 1]; + data[i + 2] = 255 - data[i + 2]; + } + + tempContext.putImageData(imageData, 0, 0); + context.drawImage( + tempCanvas, + 0, + 0, + tempCanvas.width, + tempCanvas.height, + 0, + 0, + element.width, + element.height, + ); + } + } else { + if (shouldInvertImage) { + context.filter = DARK_THEME_FILTER; + } + + context.drawImage( + img, + x, + y, + width, + height, + 0 /* hardcoded for the selection box*/, + 0, + element.width, + element.height, + ); + } } else { - drawImagePlaceholder(element, context); + drawImagePlaceholder(element, context, renderConfig.theme); } context.restore(); break; From 94364af68fbc88ced8d807e8e9534d23d474479e Mon Sep 17 00:00:00 2001 From: Yash Date: Sat, 31 Jan 2026 21:45:14 +0530 Subject: [PATCH 10/32] fix: Clarify welcome screen message about browser storage limitations (#10721) * fix: Clarify welcome screen message about browser storage limitations * css tweaks * update snaps --------- Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com> --- excalidraw-app/components/AppWelcomeScreen.tsx | 10 +++++++++- .../tests/__snapshots__/MobileMenu.test.tsx.snap | 6 +++++- .../components/welcome-screen/WelcomeScreen.scss | 3 ++- packages/excalidraw/locales/en.json | 4 +++- 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/excalidraw-app/components/AppWelcomeScreen.tsx b/excalidraw-app/components/AppWelcomeScreen.tsx index 5bb5073e9c..903af7a5f7 100644 --- a/excalidraw-app/components/AppWelcomeScreen.tsx +++ b/excalidraw-app/components/AppWelcomeScreen.tsx @@ -33,7 +33,15 @@ export const AppWelcomeScreen: React.FC<{ return bit; }); } else { - headingContent = t("welcomeScreen.app.center_heading"); + headingContent = ( + <> + {t("welcomeScreen.app.center_heading")} +
+ {t("welcomeScreen.app.center_heading_line2")} +
+ {t("welcomeScreen.app.center_heading_line3")} + + ); } return ( diff --git a/excalidraw-app/tests/__snapshots__/MobileMenu.test.tsx.snap b/excalidraw-app/tests/__snapshots__/MobileMenu.test.tsx.snap index 53b16c40ff..8bdc0f99ff 100644 --- a/excalidraw-app/tests/__snapshots__/MobileMenu.test.tsx.snap +++ b/excalidraw-app/tests/__snapshots__/MobileMenu.test.tsx.snap @@ -50,7 +50,11 @@ exports[`Test MobileMenu > should initialize with welcome screen and hide once u
- All your data is saved locally in your browser. + Your drawings are saved in your browser's storage. +
+ Browser storage can be cleared unexpectedly. +
+ Save your work to a file regularly to avoid losing it.
Date: Sat, 31 Jan 2026 20:24:20 +0100 Subject: [PATCH 11/32] feat(packages/excalidraw): export CommandPalette (#10724) feat: export CommandPalette --- packages/excalidraw/index.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/excalidraw/index.tsx b/packages/excalidraw/index.tsx index db0fe94cd8..f8004b4c2f 100644 --- a/packages/excalidraw/index.tsx +++ b/packages/excalidraw/index.tsx @@ -317,3 +317,5 @@ export { getDataURL } from "./data/blob"; export { isElementLink } from "@excalidraw/element"; export { setCustomTextMetricsProvider } from "@excalidraw/element"; + +export { CommandPalette } from "./components/CommandPalette/CommandPalette"; From f12ae80ba1e1364952c6c6c440ac4212acd06eab Mon Sep 17 00:00:00 2001 From: Excalidraw Bot <77840495+excalibot@users.noreply.github.com> Date: Sat, 31 Jan 2026 22:12:24 +0100 Subject: [PATCH 12/32] chore: Update translations from Crowdin (#10598) * New translations en.json (Russian) * New translations en.json (Vietnamese) * New translations en.json (Russian) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Arabic) * New translations en.json (Bulgarian) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Basque) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Kurdish) * New translations en.json (Lithuanian) * New translations en.json (Dutch) * New translations en.json (Punjabi) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Slovak) * New translations en.json (Slovenian) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Galician) * New translations en.json (Portuguese, Brazilian) * New translations en.json (Indonesian) * New translations en.json (Persian) * New translations en.json (Khmer) * New translations en.json (Tamil) * New translations en.json (Bengali) * New translations en.json (Marathi) * New translations en.json (Thai) * New translations en.json (Norwegian Nynorsk) * New translations en.json (Kazakh) * New translations en.json (Latvian) * New translations en.json (Azerbaijani) * New translations en.json (Hindi) * New translations en.json (Burmese) * New translations en.json (Chinese Traditional, Hong Kong) * New translations en.json (Sinhala) * New translations en.json (Uzbek) * New translations en.json (Norwegian Bokmal) * New translations en.json (Occitan) * New translations en.json (German, Switzerland) * New translations en.json (Bengali, India) * New translations en.json (Kabyle) * New translations en.json (Karakalpak) * Auto commit: Calculate translation coverage * New translations en.json (Russian) * Auto commit: Calculate translation coverage * New translations en.json (Romanian) * Auto commit: Calculate translation coverage * New translations en.json (Italian) * Auto commit: Calculate translation coverage * New translations en.json (Italian) * Auto commit: Calculate translation coverage * New translations en.json (Hungarian) * New translations en.json (Hungarian) * New translations en.json (Hindi) * New translations en.json (Dutch) * New translations en.json (Dutch) * New translations en.json (Vietnamese) * New translations en.json (Russian) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Arabic) * New translations en.json (Bulgarian) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Basque) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Kurdish) * New translations en.json (Lithuanian) * New translations en.json (Dutch) * New translations en.json (Punjabi) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Slovak) * New translations en.json (Slovenian) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Galician) * New translations en.json (Portuguese, Brazilian) * New translations en.json (Indonesian) * New translations en.json (Persian) * New translations en.json (Khmer) * New translations en.json (Tamil) * New translations en.json (Bengali) * New translations en.json (Marathi) * New translations en.json (Thai) * New translations en.json (Norwegian Nynorsk) * New translations en.json (Kazakh) * New translations en.json (Latvian) * New translations en.json (Azerbaijani) * New translations en.json (Hindi) * New translations en.json (Burmese) * New translations en.json (Chinese Traditional, Hong Kong) * New translations en.json (Sinhala) * New translations en.json (Uzbek) * New translations en.json (Norwegian Bokmal) * New translations en.json (Occitan) * New translations en.json (German, Switzerland) * New translations en.json (Bengali, India) * New translations en.json (Kabyle) * New translations en.json (Karakalpak) * New translations en.json (Romanian) * New translations en.json (Vietnamese) * New translations en.json (Russian) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Arabic) * New translations en.json (Bulgarian) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Basque) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Kurdish) * New translations en.json (Lithuanian) * New translations en.json (Dutch) * New translations en.json (Punjabi) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Slovak) * New translations en.json (Slovenian) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Galician) * New translations en.json (Portuguese, Brazilian) * New translations en.json (Indonesian) * New translations en.json (Persian) * New translations en.json (Khmer) * New translations en.json (Tamil) * New translations en.json (Bengali) * New translations en.json (Marathi) * New translations en.json (Thai) * New translations en.json (Norwegian Nynorsk) * New translations en.json (Kazakh) * New translations en.json (Latvian) * New translations en.json (Azerbaijani) * New translations en.json (Hindi) * New translations en.json (Burmese) * New translations en.json (Chinese Traditional, Hong Kong) * New translations en.json (Sinhala) * New translations en.json (Uzbek) * New translations en.json (Norwegian Bokmal) * New translations en.json (Occitan) * New translations en.json (German, Switzerland) * New translations en.json (Bengali, India) * New translations en.json (Kabyle) * New translations en.json (Karakalpak) * New translations en.json (Romanian) * New translations en.json (Italian) * New translations en.json (Russian) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Arabic) * New translations en.json (Bulgarian) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Basque) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Kurdish) * New translations en.json (Lithuanian) * New translations en.json (Dutch) * New translations en.json (Punjabi) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Slovak) * New translations en.json (Slovenian) * New translations en.json (Swedish) * New translations en.json (Vietnamese) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Galician) * New translations en.json (Portuguese, Brazilian) * New translations en.json (Indonesian) * New translations en.json (Persian) * New translations en.json (Khmer) * New translations en.json (Tamil) * New translations en.json (Bengali) * New translations en.json (Marathi) * New translations en.json (Thai) * New translations en.json (Norwegian Nynorsk) * New translations en.json (Kazakh) * New translations en.json (Latvian) * New translations en.json (Azerbaijani) * New translations en.json (Hindi) * New translations en.json (Burmese) * New translations en.json (Chinese Traditional, Hong Kong) * New translations en.json (Sinhala) * New translations en.json (Uzbek) * New translations en.json (Norwegian Bokmal) * New translations en.json (Occitan) * New translations en.json (German, Switzerland) * New translations en.json (Bengali, India) * New translations en.json (Kabyle) * New translations en.json (Karakalpak) * New translations en.json (Romanian) * Auto commit: Calculate translation coverage --- packages/excalidraw/locales/ar-SA.json | 54 ++++- packages/excalidraw/locales/az-AZ.json | 52 ++++- packages/excalidraw/locales/bg-BG.json | 54 ++++- packages/excalidraw/locales/bn-BD.json | 52 ++++- packages/excalidraw/locales/bn-IN.json | 52 ++++- packages/excalidraw/locales/ca-ES.json | 54 ++++- packages/excalidraw/locales/cs-CZ.json | 54 ++++- packages/excalidraw/locales/da-DK.json | 52 ++++- packages/excalidraw/locales/de-CH.json | 66 +++++- packages/excalidraw/locales/de-DE.json | 54 ++++- packages/excalidraw/locales/el-GR.json | 54 ++++- packages/excalidraw/locales/es-ES.json | 54 ++++- packages/excalidraw/locales/eu-ES.json | 54 ++++- packages/excalidraw/locales/fa-IR.json | 54 ++++- packages/excalidraw/locales/fi-FI.json | 54 ++++- packages/excalidraw/locales/fr-FR.json | 54 ++++- packages/excalidraw/locales/gl-ES.json | 54 ++++- packages/excalidraw/locales/he-IL.json | 54 ++++- packages/excalidraw/locales/hi-IN.json | 72 +++++- packages/excalidraw/locales/hu-HU.json | 232 +++++++++++-------- packages/excalidraw/locales/id-ID.json | 54 ++++- packages/excalidraw/locales/it-IT.json | 54 ++++- packages/excalidraw/locales/ja-JP.json | 54 ++++- packages/excalidraw/locales/kaa.json | 52 ++++- packages/excalidraw/locales/kab-KAB.json | 54 ++++- packages/excalidraw/locales/kk-KZ.json | 52 ++++- packages/excalidraw/locales/km-KH.json | 54 ++++- packages/excalidraw/locales/ko-KR.json | 54 ++++- packages/excalidraw/locales/ku-TR.json | 54 ++++- packages/excalidraw/locales/lt-LT.json | 52 ++++- packages/excalidraw/locales/lv-LV.json | 54 ++++- packages/excalidraw/locales/mr-IN.json | 54 ++++- packages/excalidraw/locales/my-MM.json | 52 ++++- packages/excalidraw/locales/nb-NO.json | 54 ++++- packages/excalidraw/locales/nl-NL.json | 56 ++++- packages/excalidraw/locales/nn-NO.json | 52 ++++- packages/excalidraw/locales/oc-FR.json | 54 ++++- packages/excalidraw/locales/pa-IN.json | 54 ++++- packages/excalidraw/locales/percentages.json | 106 ++++----- packages/excalidraw/locales/pl-PL.json | 54 ++++- packages/excalidraw/locales/pt-BR.json | 54 ++++- packages/excalidraw/locales/pt-PT.json | 54 ++++- packages/excalidraw/locales/ro-RO.json | 54 ++++- packages/excalidraw/locales/ru-RU.json | 58 ++++- packages/excalidraw/locales/si-LK.json | 54 ++++- packages/excalidraw/locales/sk-SK.json | 54 ++++- packages/excalidraw/locales/sl-SI.json | 54 ++++- packages/excalidraw/locales/sv-SE.json | 54 ++++- packages/excalidraw/locales/ta-IN.json | 52 ++++- packages/excalidraw/locales/th-TH.json | 52 ++++- packages/excalidraw/locales/tr-TR.json | 54 ++++- packages/excalidraw/locales/uk-UA.json | 54 ++++- packages/excalidraw/locales/uz-UZ.json | 52 ++++- packages/excalidraw/locales/vi-VN.json | 52 ++++- packages/excalidraw/locales/zh-CN.json | 54 ++++- packages/excalidraw/locales/zh-HK.json | 52 ++++- packages/excalidraw/locales/zh-TW.json | 54 ++++- 57 files changed, 3058 insertions(+), 258 deletions(-) diff --git a/packages/excalidraw/locales/ar-SA.json b/packages/excalidraw/locales/ar-SA.json index e2380a942c..e135979ad0 100644 --- a/packages/excalidraw/locales/ar-SA.json +++ b/packages/excalidraw/locales/ar-SA.json @@ -557,7 +557,9 @@ }, "welcomeScreen": { "app": { - "center_heading": "جميع بياناتك محفوظة محليًا في المتصفح الخاص بك.", + "center_heading": "", + "center_heading_line2": "", + "center_heading_line3": "", "center_heading_plus": "هل تريد الانتقال إلى Excalidraw+ بدلاً من ذلك؟", "menuHint": "التصدير، التفضيلات، اللغات..." }, @@ -612,7 +614,55 @@ "button": "إدراج", "description": "حاليًا، يتم دعم مخططات التدفق، التسلسلات، والفئات فقط. سيتم عرض الأنواع الأخرى كصورة في Excalidraw.", "syntax": "صيغة Mermaid", - "preview": "معاينة" + "preview": "معاينة", + "label": "", + "inputPlaceholder": "" + }, + "ttd": { + "error": "" + }, + "chat": { + "inputPlaceholder": "", + "inputPlaceholderWithMessages": "", + "generating": "", + "rateLimitRemaining": "", + "role": { + "user": "", + "assistant": "", + "system": "" + }, + "aiBeta": "", + "label": "", + "menu": "", + "newChat": "", + "deleteChat": "", + "deleteMessage": "", + "viewAsMermaid": "", + "placeholder": { + "title": "", + "description": "", + "hint": "" + }, + "preview": "", + "insert": "", + "retry": "", + "errors": { + "promptTooShort": "", + "promptTooLong": "", + "generationFailed": "", + "invalidDiagram": "", + "fixInMermaid": "", + "aiRepair": "", + "requestAborted": "", + "requestFailed": "", + "mermaidParseError": "" + }, + "rateLimit": { + "messageLimit": "", + "generalRateLimit": "", + "messageLimitInputPlaceholder": "" + }, + "upsellBtnLabel": "" }, "quickSearch": { "placeholder": "بحث سريع" diff --git a/packages/excalidraw/locales/az-AZ.json b/packages/excalidraw/locales/az-AZ.json index 2a9a654c80..dc9237e44f 100644 --- a/packages/excalidraw/locales/az-AZ.json +++ b/packages/excalidraw/locales/az-AZ.json @@ -558,6 +558,8 @@ "welcomeScreen": { "app": { "center_heading": "", + "center_heading_line2": "", + "center_heading_line3": "", "center_heading_plus": "", "menuHint": "" }, @@ -612,7 +614,55 @@ "button": "", "description": "", "syntax": "", - "preview": "" + "preview": "", + "label": "", + "inputPlaceholder": "" + }, + "ttd": { + "error": "" + }, + "chat": { + "inputPlaceholder": "", + "inputPlaceholderWithMessages": "", + "generating": "", + "rateLimitRemaining": "", + "role": { + "user": "", + "assistant": "", + "system": "" + }, + "aiBeta": "", + "label": "", + "menu": "", + "newChat": "", + "deleteChat": "", + "deleteMessage": "", + "viewAsMermaid": "", + "placeholder": { + "title": "", + "description": "", + "hint": "" + }, + "preview": "", + "insert": "", + "retry": "", + "errors": { + "promptTooShort": "", + "promptTooLong": "", + "generationFailed": "", + "invalidDiagram": "", + "fixInMermaid": "", + "aiRepair": "", + "requestAborted": "", + "requestFailed": "", + "mermaidParseError": "" + }, + "rateLimit": { + "messageLimit": "", + "generalRateLimit": "", + "messageLimitInputPlaceholder": "" + }, + "upsellBtnLabel": "" }, "quickSearch": { "placeholder": "" diff --git a/packages/excalidraw/locales/bg-BG.json b/packages/excalidraw/locales/bg-BG.json index eafef1c9d8..118733d7c9 100644 --- a/packages/excalidraw/locales/bg-BG.json +++ b/packages/excalidraw/locales/bg-BG.json @@ -557,7 +557,9 @@ }, "welcomeScreen": { "app": { - "center_heading": "Всичките Ви данни са запазени локално в браузъра Ви.", + "center_heading": "", + "center_heading_line2": "", + "center_heading_line3": "", "center_heading_plus": "", "menuHint": "Експорт, предпочитания, езици, ..." }, @@ -612,7 +614,55 @@ "button": "Вмъкни", "description": "", "syntax": "Mermaid Синтаксис", - "preview": "Преглед" + "preview": "Преглед", + "label": "", + "inputPlaceholder": "" + }, + "ttd": { + "error": "" + }, + "chat": { + "inputPlaceholder": "", + "inputPlaceholderWithMessages": "", + "generating": "", + "rateLimitRemaining": "", + "role": { + "user": "", + "assistant": "", + "system": "" + }, + "aiBeta": "", + "label": "", + "menu": "", + "newChat": "", + "deleteChat": "", + "deleteMessage": "", + "viewAsMermaid": "", + "placeholder": { + "title": "", + "description": "", + "hint": "" + }, + "preview": "", + "insert": "", + "retry": "", + "errors": { + "promptTooShort": "", + "promptTooLong": "", + "generationFailed": "", + "invalidDiagram": "", + "fixInMermaid": "", + "aiRepair": "", + "requestAborted": "", + "requestFailed": "", + "mermaidParseError": "" + }, + "rateLimit": { + "messageLimit": "", + "generalRateLimit": "", + "messageLimitInputPlaceholder": "" + }, + "upsellBtnLabel": "" }, "quickSearch": { "placeholder": "" diff --git a/packages/excalidraw/locales/bn-BD.json b/packages/excalidraw/locales/bn-BD.json index a6d48de209..3b69089413 100644 --- a/packages/excalidraw/locales/bn-BD.json +++ b/packages/excalidraw/locales/bn-BD.json @@ -558,6 +558,8 @@ "welcomeScreen": { "app": { "center_heading": "", + "center_heading_line2": "", + "center_heading_line3": "", "center_heading_plus": "", "menuHint": "" }, @@ -612,7 +614,55 @@ "button": "", "description": "", "syntax": "", - "preview": "" + "preview": "", + "label": "", + "inputPlaceholder": "" + }, + "ttd": { + "error": "" + }, + "chat": { + "inputPlaceholder": "", + "inputPlaceholderWithMessages": "", + "generating": "", + "rateLimitRemaining": "", + "role": { + "user": "", + "assistant": "", + "system": "" + }, + "aiBeta": "", + "label": "", + "menu": "", + "newChat": "", + "deleteChat": "", + "deleteMessage": "", + "viewAsMermaid": "", + "placeholder": { + "title": "", + "description": "", + "hint": "" + }, + "preview": "", + "insert": "", + "retry": "", + "errors": { + "promptTooShort": "", + "promptTooLong": "", + "generationFailed": "", + "invalidDiagram": "", + "fixInMermaid": "", + "aiRepair": "", + "requestAborted": "", + "requestFailed": "", + "mermaidParseError": "" + }, + "rateLimit": { + "messageLimit": "", + "generalRateLimit": "", + "messageLimitInputPlaceholder": "" + }, + "upsellBtnLabel": "" }, "quickSearch": { "placeholder": "" diff --git a/packages/excalidraw/locales/bn-IN.json b/packages/excalidraw/locales/bn-IN.json index a6d48de209..3b69089413 100644 --- a/packages/excalidraw/locales/bn-IN.json +++ b/packages/excalidraw/locales/bn-IN.json @@ -558,6 +558,8 @@ "welcomeScreen": { "app": { "center_heading": "", + "center_heading_line2": "", + "center_heading_line3": "", "center_heading_plus": "", "menuHint": "" }, @@ -612,7 +614,55 @@ "button": "", "description": "", "syntax": "", - "preview": "" + "preview": "", + "label": "", + "inputPlaceholder": "" + }, + "ttd": { + "error": "" + }, + "chat": { + "inputPlaceholder": "", + "inputPlaceholderWithMessages": "", + "generating": "", + "rateLimitRemaining": "", + "role": { + "user": "", + "assistant": "", + "system": "" + }, + "aiBeta": "", + "label": "", + "menu": "", + "newChat": "", + "deleteChat": "", + "deleteMessage": "", + "viewAsMermaid": "", + "placeholder": { + "title": "", + "description": "", + "hint": "" + }, + "preview": "", + "insert": "", + "retry": "", + "errors": { + "promptTooShort": "", + "promptTooLong": "", + "generationFailed": "", + "invalidDiagram": "", + "fixInMermaid": "", + "aiRepair": "", + "requestAborted": "", + "requestFailed": "", + "mermaidParseError": "" + }, + "rateLimit": { + "messageLimit": "", + "generalRateLimit": "", + "messageLimitInputPlaceholder": "" + }, + "upsellBtnLabel": "" }, "quickSearch": { "placeholder": "" diff --git a/packages/excalidraw/locales/ca-ES.json b/packages/excalidraw/locales/ca-ES.json index cfb463e404..f674375148 100644 --- a/packages/excalidraw/locales/ca-ES.json +++ b/packages/excalidraw/locales/ca-ES.json @@ -557,7 +557,9 @@ }, "welcomeScreen": { "app": { - "center_heading": "Totes les vostres dades es guarden localment al vostre navegador.", + "center_heading": "", + "center_heading_line2": "", + "center_heading_line3": "", "center_heading_plus": "Vols anar a Excalidraw+ en comptes?", "menuHint": "Exportar, preferències, llenguatges..." }, @@ -612,7 +614,55 @@ "button": "Inseriu", "description": "Actualment només s'admeten els diagrames Flowchart, Sequence, i Class . Els altres tipus es representaran com a imatge a Excalidraw.", "syntax": "Sintaxi de Mermaid", - "preview": "Previsualització" + "preview": "Previsualització", + "label": "", + "inputPlaceholder": "" + }, + "ttd": { + "error": "" + }, + "chat": { + "inputPlaceholder": "", + "inputPlaceholderWithMessages": "", + "generating": "", + "rateLimitRemaining": "", + "role": { + "user": "", + "assistant": "", + "system": "" + }, + "aiBeta": "", + "label": "", + "menu": "", + "newChat": "", + "deleteChat": "", + "deleteMessage": "", + "viewAsMermaid": "", + "placeholder": { + "title": "", + "description": "", + "hint": "" + }, + "preview": "", + "insert": "", + "retry": "", + "errors": { + "promptTooShort": "", + "promptTooLong": "", + "generationFailed": "", + "invalidDiagram": "", + "fixInMermaid": "", + "aiRepair": "", + "requestAborted": "", + "requestFailed": "", + "mermaidParseError": "" + }, + "rateLimit": { + "messageLimit": "", + "generalRateLimit": "", + "messageLimitInputPlaceholder": "" + }, + "upsellBtnLabel": "" }, "quickSearch": { "placeholder": "Cerca ràpida" diff --git a/packages/excalidraw/locales/cs-CZ.json b/packages/excalidraw/locales/cs-CZ.json index 3d93b1cd83..6c0f84afea 100644 --- a/packages/excalidraw/locales/cs-CZ.json +++ b/packages/excalidraw/locales/cs-CZ.json @@ -557,7 +557,9 @@ }, "welcomeScreen": { "app": { - "center_heading": "Všechna vaše data jsou uložena lokálně ve vašem prohlížeči.", + "center_heading": "", + "center_heading_line2": "", + "center_heading_line3": "", "center_heading_plus": "Chcete místo toho přejít na Excalidraw+?", "menuHint": "Export, nastavení, jazyky, ..." }, @@ -612,7 +614,55 @@ "button": "Vložit", "description": "", "syntax": "Mermaid syntaxe", - "preview": "Náhled" + "preview": "Náhled", + "label": "", + "inputPlaceholder": "" + }, + "ttd": { + "error": "" + }, + "chat": { + "inputPlaceholder": "", + "inputPlaceholderWithMessages": "", + "generating": "", + "rateLimitRemaining": "", + "role": { + "user": "", + "assistant": "", + "system": "" + }, + "aiBeta": "", + "label": "", + "menu": "", + "newChat": "", + "deleteChat": "", + "deleteMessage": "", + "viewAsMermaid": "", + "placeholder": { + "title": "", + "description": "", + "hint": "" + }, + "preview": "", + "insert": "", + "retry": "", + "errors": { + "promptTooShort": "", + "promptTooLong": "", + "generationFailed": "", + "invalidDiagram": "", + "fixInMermaid": "", + "aiRepair": "", + "requestAborted": "", + "requestFailed": "", + "mermaidParseError": "" + }, + "rateLimit": { + "messageLimit": "", + "generalRateLimit": "", + "messageLimitInputPlaceholder": "" + }, + "upsellBtnLabel": "" }, "quickSearch": { "placeholder": "Rychlé vyhledávání" diff --git a/packages/excalidraw/locales/da-DK.json b/packages/excalidraw/locales/da-DK.json index 111aa91261..5915105db6 100644 --- a/packages/excalidraw/locales/da-DK.json +++ b/packages/excalidraw/locales/da-DK.json @@ -558,6 +558,8 @@ "welcomeScreen": { "app": { "center_heading": "", + "center_heading_line2": "", + "center_heading_line3": "", "center_heading_plus": "", "menuHint": "" }, @@ -612,7 +614,55 @@ "button": "", "description": "", "syntax": "", - "preview": "" + "preview": "", + "label": "", + "inputPlaceholder": "" + }, + "ttd": { + "error": "" + }, + "chat": { + "inputPlaceholder": "", + "inputPlaceholderWithMessages": "", + "generating": "", + "rateLimitRemaining": "", + "role": { + "user": "", + "assistant": "", + "system": "" + }, + "aiBeta": "", + "label": "", + "menu": "", + "newChat": "", + "deleteChat": "", + "deleteMessage": "", + "viewAsMermaid": "", + "placeholder": { + "title": "", + "description": "", + "hint": "" + }, + "preview": "", + "insert": "", + "retry": "", + "errors": { + "promptTooShort": "", + "promptTooLong": "", + "generationFailed": "", + "invalidDiagram": "", + "fixInMermaid": "", + "aiRepair": "", + "requestAborted": "", + "requestFailed": "", + "mermaidParseError": "" + }, + "rateLimit": { + "messageLimit": "", + "generalRateLimit": "", + "messageLimitInputPlaceholder": "" + }, + "upsellBtnLabel": "" }, "quickSearch": { "placeholder": "" diff --git a/packages/excalidraw/locales/de-CH.json b/packages/excalidraw/locales/de-CH.json index c8148eb49a..047fc98e4b 100644 --- a/packages/excalidraw/locales/de-CH.json +++ b/packages/excalidraw/locales/de-CH.json @@ -196,7 +196,7 @@ "multipleResults": "Ergebnisse", "placeholder": "Text auf Zeichenfläche suchen...", "frames": "", - "texts": "" + "texts": "Texte" }, "buttons": { "clearReset": "Zeichenfläche löschen & Hintergrundfarbe zurücksetzen", @@ -557,7 +557,9 @@ }, "welcomeScreen": { "app": { - "center_heading": "Alle Daten werden lokal in Deinem Browser gespeichert.", + "center_heading": "", + "center_heading_line2": "", + "center_heading_line3": "", "center_heading_plus": "Möchtest du stattdessen zu Excalidraw+ gehen?", "menuHint": "Exportieren, Einstellungen, Sprachen, ..." }, @@ -569,7 +571,7 @@ } }, "colorPicker": { - "color": "", + "color": "Farbe", "mostUsedCustomColors": "Beliebteste benutzerdefinierte Farben", "colors": "Farben", "shades": "Schattierungen", @@ -612,7 +614,55 @@ "button": "Einfügen", "description": "Derzeit werden nur Flussdiagramme, Sequenzdiagramme und Klassendiagramme unterstützt. Die anderen Typen werden als Bild in Excalidraw dargestellt.", "syntax": "Mermaid-Syntax", - "preview": "Vorschau" + "preview": "Vorschau", + "label": "", + "inputPlaceholder": "" + }, + "ttd": { + "error": "" + }, + "chat": { + "inputPlaceholder": "", + "inputPlaceholderWithMessages": "", + "generating": "", + "rateLimitRemaining": "", + "role": { + "user": "", + "assistant": "", + "system": "" + }, + "aiBeta": "", + "label": "", + "menu": "", + "newChat": "", + "deleteChat": "", + "deleteMessage": "", + "viewAsMermaid": "", + "placeholder": { + "title": "", + "description": "", + "hint": "" + }, + "preview": "", + "insert": "", + "retry": "", + "errors": { + "promptTooShort": "", + "promptTooLong": "", + "generationFailed": "", + "invalidDiagram": "", + "fixInMermaid": "", + "aiRepair": "", + "requestAborted": "", + "requestFailed": "", + "mermaidParseError": "" + }, + "rateLimit": { + "messageLimit": "", + "generalRateLimit": "", + "messageLimitInputPlaceholder": "" + }, + "upsellBtnLabel": "" }, "quickSearch": { "placeholder": "Schnellsuche" @@ -651,15 +701,15 @@ "shortcutHint": "Benutze {{shortcut}} für Befehlspalette" }, "keys": { - "ctrl": "", + "ctrl": "Strg", "option": "", "cmd": "", "alt": "", "escape": "", "enter": "", "shift": "", - "spacebar": "", - "delete": "", - "mmb": "" + "spacebar": "Leertaste", + "delete": "Löschen", + "mmb": "Mausrad" } } diff --git a/packages/excalidraw/locales/de-DE.json b/packages/excalidraw/locales/de-DE.json index 719d91c526..047fc98e4b 100644 --- a/packages/excalidraw/locales/de-DE.json +++ b/packages/excalidraw/locales/de-DE.json @@ -557,7 +557,9 @@ }, "welcomeScreen": { "app": { - "center_heading": "Alle Daten werden lokal in Deinem Browser gespeichert.", + "center_heading": "", + "center_heading_line2": "", + "center_heading_line3": "", "center_heading_plus": "Möchtest du stattdessen zu Excalidraw+ gehen?", "menuHint": "Exportieren, Einstellungen, Sprachen, ..." }, @@ -612,7 +614,55 @@ "button": "Einfügen", "description": "Derzeit werden nur Flussdiagramme, Sequenzdiagramme und Klassendiagramme unterstützt. Die anderen Typen werden als Bild in Excalidraw dargestellt.", "syntax": "Mermaid-Syntax", - "preview": "Vorschau" + "preview": "Vorschau", + "label": "", + "inputPlaceholder": "" + }, + "ttd": { + "error": "" + }, + "chat": { + "inputPlaceholder": "", + "inputPlaceholderWithMessages": "", + "generating": "", + "rateLimitRemaining": "", + "role": { + "user": "", + "assistant": "", + "system": "" + }, + "aiBeta": "", + "label": "", + "menu": "", + "newChat": "", + "deleteChat": "", + "deleteMessage": "", + "viewAsMermaid": "", + "placeholder": { + "title": "", + "description": "", + "hint": "" + }, + "preview": "", + "insert": "", + "retry": "", + "errors": { + "promptTooShort": "", + "promptTooLong": "", + "generationFailed": "", + "invalidDiagram": "", + "fixInMermaid": "", + "aiRepair": "", + "requestAborted": "", + "requestFailed": "", + "mermaidParseError": "" + }, + "rateLimit": { + "messageLimit": "", + "generalRateLimit": "", + "messageLimitInputPlaceholder": "" + }, + "upsellBtnLabel": "" }, "quickSearch": { "placeholder": "Schnellsuche" diff --git a/packages/excalidraw/locales/el-GR.json b/packages/excalidraw/locales/el-GR.json index 5154329b21..b8c93b1112 100644 --- a/packages/excalidraw/locales/el-GR.json +++ b/packages/excalidraw/locales/el-GR.json @@ -557,7 +557,9 @@ }, "welcomeScreen": { "app": { - "center_heading": "Όλα τα δεδομένα σας αποθηκεύονται τοπικά στο πρόγραμμα περιήγησης.", + "center_heading": "", + "center_heading_line2": "", + "center_heading_line3": "", "center_heading_plus": "Μήπως θέλατε να πάτε στο Excalidraw+;", "menuHint": "Εξαγωγή, προτιμήσεις, γλώσσες, ..." }, @@ -612,7 +614,55 @@ "button": "Εισαγωγή", "description": "Επί του παρόντος υποστηρίζονται μόνο Διαγράμματα Ροής, Ακολουθίας, και Κλάσεων. Οι άλλοι τύποι θα αποδοθούν ως εικόνα στο Excalidraw.", "syntax": "Σύνταξη Mermaid", - "preview": "Προεπισκόπηση" + "preview": "Προεπισκόπηση", + "label": "", + "inputPlaceholder": "" + }, + "ttd": { + "error": "" + }, + "chat": { + "inputPlaceholder": "", + "inputPlaceholderWithMessages": "", + "generating": "", + "rateLimitRemaining": "", + "role": { + "user": "", + "assistant": "", + "system": "" + }, + "aiBeta": "", + "label": "", + "menu": "", + "newChat": "", + "deleteChat": "", + "deleteMessage": "", + "viewAsMermaid": "", + "placeholder": { + "title": "", + "description": "", + "hint": "" + }, + "preview": "", + "insert": "", + "retry": "", + "errors": { + "promptTooShort": "", + "promptTooLong": "", + "generationFailed": "", + "invalidDiagram": "", + "fixInMermaid": "", + "aiRepair": "", + "requestAborted": "", + "requestFailed": "", + "mermaidParseError": "" + }, + "rateLimit": { + "messageLimit": "", + "generalRateLimit": "", + "messageLimitInputPlaceholder": "" + }, + "upsellBtnLabel": "" }, "quickSearch": { "placeholder": "Γρήγορη αναζήτηση" diff --git a/packages/excalidraw/locales/es-ES.json b/packages/excalidraw/locales/es-ES.json index c05da9af51..811d5b77e7 100644 --- a/packages/excalidraw/locales/es-ES.json +++ b/packages/excalidraw/locales/es-ES.json @@ -557,7 +557,9 @@ }, "welcomeScreen": { "app": { - "center_heading": "Toda su información es guardada localmente en su navegador.", + "center_heading": "", + "center_heading_line2": "", + "center_heading_line3": "", "center_heading_plus": "¿Quieres ir a Excalidraw+?", "menuHint": "Exportar, preferencias, idiomas, ..." }, @@ -612,7 +614,55 @@ "button": "Insertar", "description": "Actualmente sólo estos tipos de diagrama de flujo, Secuencia, y Clase son soportados. Los otros tipos de diagramas se renderizarán como imagen en Excalidraw.", "syntax": "Sintaxis Mermaid", - "preview": "Vista previa" + "preview": "Vista previa", + "label": "", + "inputPlaceholder": "" + }, + "ttd": { + "error": "" + }, + "chat": { + "inputPlaceholder": "", + "inputPlaceholderWithMessages": "", + "generating": "", + "rateLimitRemaining": "", + "role": { + "user": "", + "assistant": "", + "system": "" + }, + "aiBeta": "", + "label": "", + "menu": "", + "newChat": "", + "deleteChat": "", + "deleteMessage": "", + "viewAsMermaid": "", + "placeholder": { + "title": "", + "description": "", + "hint": "" + }, + "preview": "", + "insert": "", + "retry": "", + "errors": { + "promptTooShort": "", + "promptTooLong": "", + "generationFailed": "", + "invalidDiagram": "", + "fixInMermaid": "", + "aiRepair": "", + "requestAborted": "", + "requestFailed": "", + "mermaidParseError": "" + }, + "rateLimit": { + "messageLimit": "", + "generalRateLimit": "", + "messageLimitInputPlaceholder": "" + }, + "upsellBtnLabel": "" }, "quickSearch": { "placeholder": "Búsqueda rápida" diff --git a/packages/excalidraw/locales/eu-ES.json b/packages/excalidraw/locales/eu-ES.json index b6f4d7e32f..e4653bb482 100644 --- a/packages/excalidraw/locales/eu-ES.json +++ b/packages/excalidraw/locales/eu-ES.json @@ -557,7 +557,9 @@ }, "welcomeScreen": { "app": { - "center_heading": "Zure datu guztiak lokalean gordetzen dira zure nabigatzailean.", + "center_heading": "", + "center_heading_line2": "", + "center_heading_line3": "", "center_heading_plus": "Horren ordez Excalidraw+-era joan nahi al zenuen?", "menuHint": "Esportatu, hobespenak, hizkuntzak..." }, @@ -612,7 +614,55 @@ "button": "Txertatu", "description": "Momentu honetan Flowchart, Sequence, eta Class Diagramak onartzen dira. Beste motak irudi gisa errendatuko dira Excalidrawn.", "syntax": "Mermaid sintaxia", - "preview": "Aurrebista" + "preview": "Aurrebista", + "label": "", + "inputPlaceholder": "" + }, + "ttd": { + "error": "" + }, + "chat": { + "inputPlaceholder": "", + "inputPlaceholderWithMessages": "", + "generating": "", + "rateLimitRemaining": "", + "role": { + "user": "", + "assistant": "", + "system": "" + }, + "aiBeta": "", + "label": "", + "menu": "", + "newChat": "", + "deleteChat": "", + "deleteMessage": "", + "viewAsMermaid": "", + "placeholder": { + "title": "", + "description": "", + "hint": "" + }, + "preview": "", + "insert": "", + "retry": "", + "errors": { + "promptTooShort": "", + "promptTooLong": "", + "generationFailed": "", + "invalidDiagram": "", + "fixInMermaid": "", + "aiRepair": "", + "requestAborted": "", + "requestFailed": "", + "mermaidParseError": "" + }, + "rateLimit": { + "messageLimit": "", + "generalRateLimit": "", + "messageLimitInputPlaceholder": "" + }, + "upsellBtnLabel": "" }, "quickSearch": { "placeholder": "" diff --git a/packages/excalidraw/locales/fa-IR.json b/packages/excalidraw/locales/fa-IR.json index ac3c553073..1bff7da6ca 100644 --- a/packages/excalidraw/locales/fa-IR.json +++ b/packages/excalidraw/locales/fa-IR.json @@ -557,7 +557,9 @@ }, "welcomeScreen": { "app": { - "center_heading": "تمام داده های شما به صورت محلی در مرورگر شما ذخیره می شود.", + "center_heading": "", + "center_heading_line2": "", + "center_heading_line3": "", "center_heading_plus": "آیا می‌خواهید به جای آن به Excalidraw+ بروید؟", "menuHint": "خروجی، ترجیحات، زبان ها، ..." }, @@ -612,7 +614,55 @@ "button": "درج", "description": "فعلا فقط فلوچارت ، توالی و کلاس نمودارها پشتیبانی می شوند. انواع دیگر به صورت تصویر در Excalidraw ارائه خواهند شد.", "syntax": "مرمید syntax", - "preview": "پیشنمایش" + "preview": "پیشنمایش", + "label": "", + "inputPlaceholder": "" + }, + "ttd": { + "error": "" + }, + "chat": { + "inputPlaceholder": "", + "inputPlaceholderWithMessages": "", + "generating": "", + "rateLimitRemaining": "", + "role": { + "user": "", + "assistant": "", + "system": "" + }, + "aiBeta": "", + "label": "", + "menu": "", + "newChat": "", + "deleteChat": "", + "deleteMessage": "", + "viewAsMermaid": "", + "placeholder": { + "title": "", + "description": "", + "hint": "" + }, + "preview": "", + "insert": "", + "retry": "", + "errors": { + "promptTooShort": "", + "promptTooLong": "", + "generationFailed": "", + "invalidDiagram": "", + "fixInMermaid": "", + "aiRepair": "", + "requestAborted": "", + "requestFailed": "", + "mermaidParseError": "" + }, + "rateLimit": { + "messageLimit": "", + "generalRateLimit": "", + "messageLimitInputPlaceholder": "" + }, + "upsellBtnLabel": "" }, "quickSearch": { "placeholder": "جستجو فوری" diff --git a/packages/excalidraw/locales/fi-FI.json b/packages/excalidraw/locales/fi-FI.json index d6052b672c..09742c471e 100644 --- a/packages/excalidraw/locales/fi-FI.json +++ b/packages/excalidraw/locales/fi-FI.json @@ -557,7 +557,9 @@ }, "welcomeScreen": { "app": { - "center_heading": "Kaikki tietosi on tallennettu paikallisesti selaimellesi.", + "center_heading": "", + "center_heading_line2": "", + "center_heading_line3": "", "center_heading_plus": "Haluatko sen sijaan mennä Excalidraw+:aan?", "menuHint": "Vie, asetukset, kielet, ..." }, @@ -612,7 +614,55 @@ "button": "", "description": "", "syntax": "", - "preview": "Esikatsele" + "preview": "Esikatsele", + "label": "", + "inputPlaceholder": "" + }, + "ttd": { + "error": "" + }, + "chat": { + "inputPlaceholder": "", + "inputPlaceholderWithMessages": "", + "generating": "", + "rateLimitRemaining": "", + "role": { + "user": "", + "assistant": "", + "system": "" + }, + "aiBeta": "", + "label": "", + "menu": "", + "newChat": "", + "deleteChat": "", + "deleteMessage": "", + "viewAsMermaid": "", + "placeholder": { + "title": "", + "description": "", + "hint": "" + }, + "preview": "", + "insert": "", + "retry": "", + "errors": { + "promptTooShort": "", + "promptTooLong": "", + "generationFailed": "", + "invalidDiagram": "", + "fixInMermaid": "", + "aiRepair": "", + "requestAborted": "", + "requestFailed": "", + "mermaidParseError": "" + }, + "rateLimit": { + "messageLimit": "", + "generalRateLimit": "", + "messageLimitInputPlaceholder": "" + }, + "upsellBtnLabel": "" }, "quickSearch": { "placeholder": "" diff --git a/packages/excalidraw/locales/fr-FR.json b/packages/excalidraw/locales/fr-FR.json index 8ec8c1282b..8bb520b660 100644 --- a/packages/excalidraw/locales/fr-FR.json +++ b/packages/excalidraw/locales/fr-FR.json @@ -557,7 +557,9 @@ }, "welcomeScreen": { "app": { - "center_heading": "Toutes vos données sont sauvegardées en local dans votre navigateur.", + "center_heading": "", + "center_heading_line2": "", + "center_heading_line3": "", "center_heading_plus": "Vouliez-vous plutôt aller à Excalidraw+ à la place ?", "menuHint": "Exportation, préférences, langues, ..." }, @@ -612,7 +614,55 @@ "button": "Insérer", "description": "Actuellement, seuls les diagrammes Flowchart, Sequence, et de classe sont pris en charge. Les autres types seront rendus en tant qu'image dans Excalidraw.", "syntax": "Syntaxe Mermaid", - "preview": "Prévisualisation" + "preview": "Prévisualisation", + "label": "", + "inputPlaceholder": "" + }, + "ttd": { + "error": "" + }, + "chat": { + "inputPlaceholder": "", + "inputPlaceholderWithMessages": "", + "generating": "", + "rateLimitRemaining": "", + "role": { + "user": "", + "assistant": "", + "system": "" + }, + "aiBeta": "", + "label": "", + "menu": "", + "newChat": "", + "deleteChat": "", + "deleteMessage": "", + "viewAsMermaid": "", + "placeholder": { + "title": "", + "description": "", + "hint": "" + }, + "preview": "", + "insert": "", + "retry": "", + "errors": { + "promptTooShort": "", + "promptTooLong": "", + "generationFailed": "", + "invalidDiagram": "", + "fixInMermaid": "", + "aiRepair": "", + "requestAborted": "", + "requestFailed": "", + "mermaidParseError": "" + }, + "rateLimit": { + "messageLimit": "", + "generalRateLimit": "", + "messageLimitInputPlaceholder": "" + }, + "upsellBtnLabel": "" }, "quickSearch": { "placeholder": "Recherche rapide" diff --git a/packages/excalidraw/locales/gl-ES.json b/packages/excalidraw/locales/gl-ES.json index bcfba4210b..fd1dc3d385 100644 --- a/packages/excalidraw/locales/gl-ES.json +++ b/packages/excalidraw/locales/gl-ES.json @@ -557,7 +557,9 @@ }, "welcomeScreen": { "app": { - "center_heading": "Toda a información é gardada de maneira local no seu navegador.", + "center_heading": "", + "center_heading_line2": "", + "center_heading_line3": "", "center_heading_plus": "Queres ir a Excalidraw+ no seu lugar?", "menuHint": "Exportar, preferencias, idiomas, ..." }, @@ -612,7 +614,55 @@ "button": "Inserir", "description": "", "syntax": "", - "preview": "Vista previa" + "preview": "Vista previa", + "label": "", + "inputPlaceholder": "" + }, + "ttd": { + "error": "" + }, + "chat": { + "inputPlaceholder": "", + "inputPlaceholderWithMessages": "", + "generating": "", + "rateLimitRemaining": "", + "role": { + "user": "", + "assistant": "", + "system": "" + }, + "aiBeta": "", + "label": "", + "menu": "", + "newChat": "", + "deleteChat": "", + "deleteMessage": "", + "viewAsMermaid": "", + "placeholder": { + "title": "", + "description": "", + "hint": "" + }, + "preview": "", + "insert": "", + "retry": "", + "errors": { + "promptTooShort": "", + "promptTooLong": "", + "generationFailed": "", + "invalidDiagram": "", + "fixInMermaid": "", + "aiRepair": "", + "requestAborted": "", + "requestFailed": "", + "mermaidParseError": "" + }, + "rateLimit": { + "messageLimit": "", + "generalRateLimit": "", + "messageLimitInputPlaceholder": "" + }, + "upsellBtnLabel": "" }, "quickSearch": { "placeholder": "" diff --git a/packages/excalidraw/locales/he-IL.json b/packages/excalidraw/locales/he-IL.json index 2e8be018e8..d09748da14 100644 --- a/packages/excalidraw/locales/he-IL.json +++ b/packages/excalidraw/locales/he-IL.json @@ -557,7 +557,9 @@ }, "welcomeScreen": { "app": { - "center_heading": "כל המידע שלח נשמר מקומית בדפדפן.", + "center_heading": "", + "center_heading_line2": "", + "center_heading_line3": "", "center_heading_plus": "אתה רוצה ללכת אל Excalidraw+ במקום?", "menuHint": "ייצוא, העדפות, שפות, ..." }, @@ -612,7 +614,55 @@ "button": "הוספה", "description": "לעת עתה נתמכים רק תרשימי זרימה, תהליכים, ודיאגרמת מחלקה. שאר הסוגים ייוצרו כתמונות ב-Excalidraw.", "syntax": "תחביר Mermaid", - "preview": "תצוגה מקדימה" + "preview": "תצוגה מקדימה", + "label": "", + "inputPlaceholder": "" + }, + "ttd": { + "error": "" + }, + "chat": { + "inputPlaceholder": "", + "inputPlaceholderWithMessages": "", + "generating": "", + "rateLimitRemaining": "", + "role": { + "user": "", + "assistant": "", + "system": "" + }, + "aiBeta": "", + "label": "", + "menu": "", + "newChat": "", + "deleteChat": "", + "deleteMessage": "", + "viewAsMermaid": "", + "placeholder": { + "title": "", + "description": "", + "hint": "" + }, + "preview": "", + "insert": "", + "retry": "", + "errors": { + "promptTooShort": "", + "promptTooLong": "", + "generationFailed": "", + "invalidDiagram": "", + "fixInMermaid": "", + "aiRepair": "", + "requestAborted": "", + "requestFailed": "", + "mermaidParseError": "" + }, + "rateLimit": { + "messageLimit": "", + "generalRateLimit": "", + "messageLimitInputPlaceholder": "" + }, + "upsellBtnLabel": "" }, "quickSearch": { "placeholder": "חיפוש מהיר" diff --git a/packages/excalidraw/locales/hi-IN.json b/packages/excalidraw/locales/hi-IN.json index 752f58501b..672dc1074d 100644 --- a/packages/excalidraw/locales/hi-IN.json +++ b/packages/excalidraw/locales/hi-IN.json @@ -143,7 +143,7 @@ }, "polygon": { "breakPolygon": "", - "convertToPolygon": "" + "convertToPolygon": "बहुभुज में कनवर्ट करें" }, "elementLock": { "lock": "ताले में रखें", @@ -171,7 +171,7 @@ "linkToElement": "वस्तु की कड़ी", "wrapSelectionInFrame": "चौकट में चुने हुवे को लपेटे", "tab": "", - "shapeSwitch": "" + "shapeSwitch": "आकार बदलें" }, "elementLink": { "title": "वस्तु की कड़ी", @@ -183,9 +183,9 @@ "hint_emptyLibrary": "यहाँ जोड़ने के लिए चित्रपटल से एक अवयव चुने, अथवा जन कोष से एक संग्रह नीचे स्थापित करें.", "hint_emptyPrivateLibrary": "यहाँ जोड़ने के लिए चित्रपटल से एक अवयव चुने.", "search": { - "inputPlaceholder": "", - "heading": "", - "noResults": "", + "inputPlaceholder": "लाइब्रेरी में खोजें", + "heading": "लाइब्रेरी मैच", + "noResults": "कोई मिलता जुलता नहीं मिला |||", "clearSearch": "" } }, @@ -195,8 +195,8 @@ "singleResult": "परिणाम", "multipleResults": "परिणाम", "placeholder": "पटल पर पाठ्य धूंडे", - "frames": "", - "texts": "" + "frames": "फ्रेम्स", + "texts": "शब्द" }, "buttons": { "clearReset": "चित्रपटल स्वच्छ करें", @@ -292,7 +292,7 @@ }, "toolBar": { "selection": "चयन", - "lasso": "", + "lasso": "लासो सलेक्शन", "image": "प्रतिमा सम्मिलित करें", "rectangle": "आयात", "diamond": "ईंट", @@ -313,7 +313,7 @@ "hand": "हाथ ( खिसका के देखने का औज़ार)", "extraTools": "अधिक उपकरण", "mermaidToExcalidraw": "मर्मेड से एक्सकाली में", - "convertElementType": "" + "convertElementType": "आकार प्रकार टॉगल करें" }, "element": { "rectangle": "आयत", @@ -557,7 +557,9 @@ }, "welcomeScreen": { "app": { - "center_heading": "आपका सर्व डेटा ब्राउज़र के भीतर स्थानिक जगह पे सुरक्षित किया गया.", + "center_heading": "", + "center_heading_line2": "", + "center_heading_line3": "", "center_heading_plus": "बजाय आपको एक्स-काली-ड्रॉ-प्लस पर जाना है?", "menuHint": "निर्यात, पसंद, भाषायें, ..." }, @@ -612,7 +614,55 @@ "button": "सन्निवेश करे", "description": "वर्तमान में केवल बहाव चित्र, अनुक्रम चित्र और वर्ग चित्र का चित्रिकरण संभव हैं. अन्य चित्र प्रकार एक्सकाली प्रतिमा जैसे चित्रित किए जायेंगे.", "syntax": "मर्मेड विन्यास", - "preview": "पूर्वावलोकन" + "preview": "पूर्वावलोकन", + "label": "", + "inputPlaceholder": "" + }, + "ttd": { + "error": "" + }, + "chat": { + "inputPlaceholder": "", + "inputPlaceholderWithMessages": "", + "generating": "", + "rateLimitRemaining": "", + "role": { + "user": "", + "assistant": "", + "system": "" + }, + "aiBeta": "", + "label": "", + "menu": "", + "newChat": "", + "deleteChat": "", + "deleteMessage": "", + "viewAsMermaid": "", + "placeholder": { + "title": "", + "description": "", + "hint": "" + }, + "preview": "", + "insert": "", + "retry": "", + "errors": { + "promptTooShort": "", + "promptTooLong": "", + "generationFailed": "", + "invalidDiagram": "", + "fixInMermaid": "", + "aiRepair": "", + "requestAborted": "", + "requestFailed": "", + "mermaidParseError": "" + }, + "rateLimit": { + "messageLimit": "", + "generalRateLimit": "", + "messageLimitInputPlaceholder": "" + }, + "upsellBtnLabel": "" }, "quickSearch": { "placeholder": "त्वरित खोज" diff --git a/packages/excalidraw/locales/hu-HU.json b/packages/excalidraw/locales/hu-HU.json index 92b8f479c6..e1ca50118f 100644 --- a/packages/excalidraw/locales/hu-HU.json +++ b/packages/excalidraw/locales/hu-HU.json @@ -11,8 +11,8 @@ "copyAsPng": "Vágólapra másolás mint PNG", "copyAsSvg": "Vágólapra másolás mint SVG", "copyText": "Vágólapra másolás szövegként", - "copySource": "", - "convertToCode": "", + "copySource": "Forrás másolása a vágólapra", + "convertToCode": "Kód generálás", "bringForward": "Előrébb hozás", "sendToBack": "Hátraküldés", "bringToFront": "Előrehozás", @@ -86,7 +86,7 @@ "layers": "Rétegek", "actions": "Műveletek", "language": "Nyelv", - "liveCollaboration": "", + "liveCollaboration": "Élő együttműködés...", "duplicateSelection": "Duplikálás", "untitled": "Névtelen", "name": "Név", @@ -95,13 +95,13 @@ "group": "Csoportosítás", "ungroup": "Csoportbontás", "collaborators": "Közreműködők", - "toggleGrid": "", + "toggleGrid": "Keresés törlése", "addToLibrary": "Hozzáadás a könyvtárhoz", "removeFromLibrary": "Eltávólítás a könyvtárból", "libraryLoadingMessage": "Könyvtár betöltése…", "libraries": "Könyvtárak böngészése", "loadingScene": "Jelenet betöltése…", - "loadScene": "", + "loadScene": "Betöltés fájlból", "align": "Igazítás", "alignTop": "Felülre igazítás", "alignBottom": "Alulra igazítás", @@ -118,7 +118,7 @@ "showStroke": "Körvonal színválasztó megjelenítése", "showBackground": "Háttérszín-választó megjelenítése", "showFonts": "", - "toggleTheme": "", + "toggleTheme": "Világos/sötét háttér kapcsoló", "theme": "Téma", "personalLib": "Személyes könyvtár", "excalidrawLib": "Excalidraw könyvtár", @@ -126,7 +126,7 @@ "increaseFontSize": "Betűméret növelése", "unbindText": "Szövegkötés feloldása", "bindText": "", - "createContainerFromText": "", + "createContainerFromText": "Szöveg bekeretezése", "link": { "edit": "Hivatkozás szerkesztése", "editEmbed": "", @@ -147,34 +147,34 @@ }, "elementLock": { "lock": "", - "unlock": "", + "unlock": "Zárolás feloldása", "lockAll": "Összes zárolása", "unlockAll": "Összes feloldása" }, "statusPublished": "Közzétéve", - "sidebarLock": "", + "sidebarLock": "Oldalsó sáv nyitva tartása", "selectAllElementsInFrame": "", "removeAllElementsFromFrame": "", "eyeDropper": "", "textToDiagram": "Szövegből diagram", - "prompt": "", + "prompt": "Egyéb", "followUs": "Kövess minket", "discordChat": "Discord chat", "zoomToFitViewport": "", - "zoomToFitSelection": "", - "zoomToFit": "", + "zoomToFitSelection": "Nagyítás a kijelölés méretére", + "zoomToFit": "Az összes elem látótérbe hozása", "installPWA": "", "autoResize": "", "imageCropping": "", "unCroppedDimension": "", "copyElementLink": "", - "linkToElement": "", + "linkToElement": "Hivatkozás az objektumhoz", "wrapSelectionInFrame": "", - "tab": "", + "tab": "Tab", "shapeSwitch": "" }, "elementLink": { - "title": "", + "title": "Hivatkozás az objektumhoz", "desc": "", "notFound": "" }, @@ -183,31 +183,31 @@ "hint_emptyLibrary": "", "hint_emptyPrivateLibrary": "", "search": { - "inputPlaceholder": "", + "inputPlaceholder": "Keresés a könyvtárban", "heading": "", "noResults": "", - "clearSearch": "" + "clearSearch": "Keresés törlése" } }, "search": { - "title": "", + "title": "Rajzvászonon használt", "noMatch": "", "singleResult": "", "multipleResults": "", "placeholder": "", - "frames": "", - "texts": "" + "frames": "Keretek", + "texts": "Szövegek" }, "buttons": { "clearReset": "Vászon törlése", "exportJSON": "Exportálás fájlba", - "exportImage": "", - "export": "", + "exportImage": "Kép exportálása...", + "export": "Mentés másként...", "copyToClipboard": "Vágólapra másolás", - "copyLink": "", + "copyLink": "Link másolása", "save": "Mentés az aktuális fájlba", "saveAs": "Mentés másként", - "load": "", + "load": "Megnyitás", "getShareableLink": "Megosztható link létrehozása", "close": "Bezárás", "selectLanguage": "Nyelv kiválasztása", @@ -225,19 +225,19 @@ "fullScreen": "Teljes képernyő", "darkMode": "Sötét mód", "lightMode": "Világos mód", - "systemMode": "", + "systemMode": "Rendszer mód", "zenMode": "Letisztult mód", - "objectsSnapMode": "", + "objectsSnapMode": "Objektumhoz illeszt", "exitZenMode": "Kilépés a letisztult módból", "cancel": "Mégsem", "saveLibNames": "", "clear": "Kiűrítés", "remove": "Eltávolítás", - "embed": "", + "embed": "Beágyazás be/ki", "publishLibrary": "", "submit": "Elküldés", "confirm": "Megerősítés", - "embeddableInteractionButton": "" + "embeddableInteractionButton": "Interakció Kattintással" }, "alerts": { "clearReset": "Ez a művelet törli a vászont. Biztos benne?", @@ -246,7 +246,7 @@ "couldNotLoadInvalidFile": "Nem sikerült betölteni a helytelen fájlt", "importBackendFailed": "Nem sikerült betölteni a szerverről.", "cannotExportEmptyCanvas": "Üres vászont nem lehet exportálni.", - "couldNotCopyToClipboard": "", + "couldNotCopyToClipboard": "Nem lehet a vágólapra másolni.", "decryptFailed": "Nem sikerült visszafejteni a titkosított adatot.", "uploadedSecurly": "A feltöltést végpontok közötti titkosítással biztosítottuk, ami azt jelenti, hogy egy harmadik fél nem tudja megnézni a tartalmát, beleértve az Excalidraw szervereit is.", "loadSceneOverridePrompt": "A betöltött külső rajz felül fogja írnia meglévőt. Szeretnéd folytatni?", @@ -304,31 +304,31 @@ "library": "Könyvtár", "lock": "Rajzolás után az aktív eszközt tartsa kijelölve", "penMode": "", - "link": "", - "eraser": "", + "link": "Hivatkozás hozzáadása/frissítése a kiválasztott alakzathoz", + "eraser": "Radír", "frame": "", "magicframe": "", "embeddable": "", - "laser": "", + "laser": "Lézermutató", "hand": "", - "extraTools": "", + "extraTools": "További eszközök", "mermaidToExcalidraw": "", "convertElementType": "" }, "element": { - "rectangle": "", - "diamond": "", - "ellipse": "", - "arrow": "", - "line": "", + "rectangle": "Téglalap", + "diamond": "Rombusz", + "ellipse": "Ellipszis", + "arrow": "Nyíl", + "line": "Vonal", "freedraw": "", - "text": "", - "image": "", - "group": "", - "frame": "", + "text": "Szöveg", + "image": "Kép", + "group": "Csoport", + "frame": "Keret", "magicframe": "", - "embeddable": "", - "selection": "", + "embeddable": "Weblap beágyazása", + "selection": "Kijelölés", "iframe": "" }, "headings": { @@ -338,7 +338,7 @@ }, "hints": { "dismissSearch": "", - "canvasPanning": "", + "canvasPanning": "A vászon mozgatásához tartsd lenyomva a {{shortcut_1}} vagy {{shortcut_2}} billentyűt húzás közben, vagy használd a kéz eszközt", "linearElement": "Kattintással görbe, az eger húzásával pedig egyenes nyilat rajzolhatsz", "arrowTool": "", "arrowBindModifiers": "", @@ -380,7 +380,7 @@ "sceneContent": "Jelenet tartalma:" }, "shareDialog": { - "or": "" + "or": "Vagy" }, "roomDialog": { "desc_intro": "", @@ -420,7 +420,7 @@ "drag": "vonszolás", "editor": "Szerkesztő", "editLineArrowPoints": "", - "editText": "", + "editText": "Szöveg szerkesztése / címke hozzáadása", "github": "Hibát találtál? Küld be", "howto": "Kövesd az útmutatóinkat", "or": "vagy", @@ -436,7 +436,7 @@ "toggleElementLock": "", "movePageUpDown": "", "movePageLeftRight": "", - "cropStart": "", + "cropStart": "Kép kivágása", "cropFinish": "" }, "clearCanvasDialog": { @@ -481,25 +481,25 @@ "imageExportDialog": { "header": "Kép exportálása", "label": { - "withBackground": "", - "onlySelected": "", - "darkMode": "", - "embedScene": "", - "scale": "", - "padding": "" + "withBackground": "Háttér", + "onlySelected": "Csak a kijelölt", + "darkMode": "Sötét mód", + "embedScene": "Jelenet beágyazása", + "scale": "Nagyítás", + "padding": "Eltartás" }, "tooltip": { - "embedScene": "" + "embedScene": "A jelenetet leíró adatok hozzá lesznek adva a PNG/SVG fájlhoz, így a jelenetet vissza lehet majd tölteni belőle. Ez megnöveli a fájl méretét." }, "title": { - "exportToPng": "", - "exportToSvg": "", - "copyPngToClipboard": "" + "exportToPng": "Exportálás PNG-be", + "exportToSvg": "Exportálás SVG-be", + "copyPngToClipboard": "PNG másolása a vágólapra" }, "button": { - "exportToPng": "", - "exportToSvg": "", - "copyPngToClipboard": "" + "exportToPng": "PNG", + "exportToSvg": "SVG", + "copyPngToClipboard": "Vágólapra másolás" } }, "encrypted": { @@ -508,14 +508,14 @@ }, "stats": { "angle": "Szög", - "shapes": "", + "shapes": "Alakzatok", "height": "Magasság", "scene": "Jelenet", "selected": "Kijelölt", "storage": "Tárhely", "fullTitle": "", - "title": "", - "generalStats": "", + "title": "Tulajdonságok", + "generalStats": "Általános", "elementProperties": "", "total": "Összesen", "version": "Verzió", @@ -540,24 +540,26 @@ }, "colors": { "transparent": "Átlátszó", - "black": "", - "white": "", - "red": "", - "pink": "", - "grape": "", - "violet": "", - "gray": "", - "blue": "", - "cyan": "", - "teal": "", - "green": "", - "yellow": "", - "orange": "", - "bronze": "" + "black": "Fekete", + "white": "Fehér", + "red": "Piros", + "pink": "Pink", + "grape": "Szőlő", + "violet": "Lila", + "gray": "Szürke", + "blue": "Kék", + "cyan": "Cián", + "teal": "Türkiz", + "green": "Zöld", + "yellow": "Sárga", + "orange": "Narancssárga", + "bronze": "Bronz" }, "welcomeScreen": { "app": { "center_heading": "", + "center_heading_line2": "", + "center_heading_line3": "", "center_heading_plus": "", "menuHint": "" }, @@ -569,7 +571,7 @@ } }, "colorPicker": { - "color": "", + "color": "Szín", "mostUsedCustomColors": "", "colors": "", "shades": "", @@ -612,18 +614,66 @@ "button": "", "description": "", "syntax": "", - "preview": "" + "preview": "", + "label": "", + "inputPlaceholder": "" + }, + "ttd": { + "error": "" + }, + "chat": { + "inputPlaceholder": "", + "inputPlaceholderWithMessages": "", + "generating": "", + "rateLimitRemaining": "", + "role": { + "user": "Te", + "assistant": "AI asszisztens", + "system": "Rendszer" + }, + "aiBeta": "", + "label": "Csevegés", + "menu": "Menü", + "newChat": "Új csevegés", + "deleteChat": "Csevegés törlése", + "deleteMessage": "Üzenet törlése", + "viewAsMermaid": "", + "placeholder": { + "title": "", + "description": "", + "hint": "" + }, + "preview": "Előnézet", + "insert": "Beszúrás", + "retry": "Újra", + "errors": { + "promptTooShort": "", + "promptTooLong": "", + "generationFailed": "", + "invalidDiagram": "", + "fixInMermaid": "", + "aiRepair": "", + "requestAborted": "", + "requestFailed": "", + "mermaidParseError": "" + }, + "rateLimit": { + "messageLimit": "", + "generalRateLimit": "", + "messageLimitInputPlaceholder": "" + }, + "upsellBtnLabel": "Frissítés Plus csomagra" }, "quickSearch": { - "placeholder": "" + "placeholder": "Gyorskeresés" }, "fontList": { "badge": { - "old": "" + "old": "régi" }, "sceneFonts": "", - "availableFonts": "", - "empty": "" + "availableFonts": "Elérhető betűtípusok", + "empty": "Nem találhatóak betűtípusok" }, "userList": { "empty": "", @@ -636,13 +686,13 @@ } }, "commandPalette": { - "title": "", + "title": "Parancspanel", "shortcuts": { - "select": "", - "confirm": "", - "close": "" + "select": "Kiválasztás", + "confirm": "Megerősítés", + "close": "Bezárás" }, - "recents": "", + "recents": "Legutóbb használt", "search": { "placeholder": "", "noMatch": "" @@ -651,8 +701,8 @@ "shortcutHint": "" }, "keys": { - "ctrl": "", - "option": "", + "ctrl": "Ctrl", + "option": "Option", "cmd": "", "alt": "", "escape": "", diff --git a/packages/excalidraw/locales/id-ID.json b/packages/excalidraw/locales/id-ID.json index 11d7fa7ba5..023f2c4319 100644 --- a/packages/excalidraw/locales/id-ID.json +++ b/packages/excalidraw/locales/id-ID.json @@ -557,7 +557,9 @@ }, "welcomeScreen": { "app": { - "center_heading": "Semua data Anda disimpan secara lokal di peramban Anda.", + "center_heading": "", + "center_heading_line2": "", + "center_heading_line3": "", "center_heading_plus": "Apa Anda ingin berpindah ke Excalidraw+?", "menuHint": "Ekspor, preferensi, bahasa, ..." }, @@ -612,7 +614,55 @@ "button": "Sisipkan", "description": "Saat ini hanya Flowchart, Sekuen, , dan KelasDiagram yang didukung. Jenis lainnya akan dirender sebagai gambar di Excalidraw.", "syntax": "Syntax Mermaid", - "preview": "Pratinjau" + "preview": "Pratinjau", + "label": "", + "inputPlaceholder": "" + }, + "ttd": { + "error": "" + }, + "chat": { + "inputPlaceholder": "", + "inputPlaceholderWithMessages": "", + "generating": "", + "rateLimitRemaining": "", + "role": { + "user": "", + "assistant": "", + "system": "" + }, + "aiBeta": "", + "label": "", + "menu": "", + "newChat": "", + "deleteChat": "", + "deleteMessage": "", + "viewAsMermaid": "", + "placeholder": { + "title": "", + "description": "", + "hint": "" + }, + "preview": "", + "insert": "", + "retry": "", + "errors": { + "promptTooShort": "", + "promptTooLong": "", + "generationFailed": "", + "invalidDiagram": "", + "fixInMermaid": "", + "aiRepair": "", + "requestAborted": "", + "requestFailed": "", + "mermaidParseError": "" + }, + "rateLimit": { + "messageLimit": "", + "generalRateLimit": "", + "messageLimitInputPlaceholder": "" + }, + "upsellBtnLabel": "" }, "quickSearch": { "placeholder": "Pencarian Cepat" diff --git a/packages/excalidraw/locales/it-IT.json b/packages/excalidraw/locales/it-IT.json index f6c73d938c..0109401424 100644 --- a/packages/excalidraw/locales/it-IT.json +++ b/packages/excalidraw/locales/it-IT.json @@ -557,7 +557,9 @@ }, "welcomeScreen": { "app": { - "center_heading": "Tutti i tuoi dati sono salvati localmente nel browser.", + "center_heading": "", + "center_heading_line2": "", + "center_heading_line3": "", "center_heading_plus": "Volevi invece andare su Excalidraw+?", "menuHint": "Esporta, preferenze, lingue, ..." }, @@ -612,7 +614,55 @@ "button": "Inserisci", "description": "Attualmente sono supportati solo diagrammi di flusso, sequenza, e classe . Gli altri tipi saranno rappresentati come immagini in Excalidraw.", "syntax": "Sintassi Mermaid", - "preview": "Anteprima" + "preview": "Anteprima", + "label": "Mermaid", + "inputPlaceholder": "Scrivi qui la definizione del diagramma Mermaid..." + }, + "ttd": { + "error": "Errore!" + }, + "chat": { + "inputPlaceholder": "Inizia a digitare qui la tua idea per il diagramma... ({{shortcut}} per una nuova riga)", + "inputPlaceholderWithMessages": "Continua a perfezionare il tuo diagramma...", + "generating": "Generazione in corso...", + "rateLimitRemaining": "{{count}} richieste rimaste oggi", + "role": { + "user": "Tu", + "assistant": "Assistente IA", + "system": "Sistema" + }, + "aiBeta": "IA Beta", + "label": "Chat", + "menu": "Menu", + "newChat": "Nuova Chat", + "deleteChat": "Elimina Chat", + "deleteMessage": "Cancella messaggio", + "viewAsMermaid": "Visualizza come Mermaid", + "placeholder": { + "title": "Progettiamo il tuo diagramma", + "description": "Descrivi il diagramma che vuoi creare e noi lo genereremo per te.", + "hint": "Al momento conosciamo i diagrammi di flusso, di sequenza e di classe." + }, + "preview": "Anteprima", + "insert": "Inserisci", + "retry": "Riprova", + "errors": { + "promptTooShort": "Il prompt è troppo corto (min {{min}} caratteri)", + "promptTooLong": "Il prompt è troppo lungo (max {{max}} caratteri)", + "generationFailed": "Generazione non riuscita", + "invalidDiagram": "È stato generato un diagramma non valido :(. Puoi modificarlo manualmente, riprovare con la correzione automatica o provare un prompt diverso.", + "fixInMermaid": "Modifica Mermaid manualmente→", + "aiRepair": "Rigenera (correzione automatica) →", + "requestAborted": "Richiesta annullata", + "requestFailed": "Richiesta non riuscita", + "mermaidParseError": "Errore di sintassi Mermaid" + }, + "rateLimit": { + "messageLimit": "Hai raggiunto il tuo limite di IA sul piano gratuito. Prova Excalidraw+ per saperne di più o torna domani.", + "generalRateLimit": "Fermati, sei troppo veloce per noi! Attendi un attimo prima di riprovare.", + "messageLimitInputPlaceholder": "Hai raggiunto il limite di messaggi" + }, + "upsellBtnLabel": "Aggiorna alla Plus" }, "quickSearch": { "placeholder": "Ricerca rapida" diff --git a/packages/excalidraw/locales/ja-JP.json b/packages/excalidraw/locales/ja-JP.json index 3c0f1e79b9..7ca89cd0b6 100644 --- a/packages/excalidraw/locales/ja-JP.json +++ b/packages/excalidraw/locales/ja-JP.json @@ -557,7 +557,9 @@ }, "welcomeScreen": { "app": { - "center_heading": "すべてのデータはブラウザにローカル保存されます。", + "center_heading": "", + "center_heading_line2": "", + "center_heading_line3": "", "center_heading_plus": "代わりにExcalidraw+を開きますか?", "menuHint": "エクスポート、設定、言語..." }, @@ -612,7 +614,55 @@ "button": "挿入", "description": "現在、FlowchartSequenceClass のダイアグラムのみに対応しています。その他の種類は、Excalidraw では画像として描画されます。", "syntax": "Mermaid 構文", - "preview": "プレビュー" + "preview": "プレビュー", + "label": "", + "inputPlaceholder": "" + }, + "ttd": { + "error": "" + }, + "chat": { + "inputPlaceholder": "", + "inputPlaceholderWithMessages": "", + "generating": "", + "rateLimitRemaining": "", + "role": { + "user": "", + "assistant": "", + "system": "" + }, + "aiBeta": "", + "label": "", + "menu": "", + "newChat": "", + "deleteChat": "", + "deleteMessage": "", + "viewAsMermaid": "", + "placeholder": { + "title": "", + "description": "", + "hint": "" + }, + "preview": "", + "insert": "", + "retry": "", + "errors": { + "promptTooShort": "", + "promptTooLong": "", + "generationFailed": "", + "invalidDiagram": "", + "fixInMermaid": "", + "aiRepair": "", + "requestAborted": "", + "requestFailed": "", + "mermaidParseError": "" + }, + "rateLimit": { + "messageLimit": "", + "generalRateLimit": "", + "messageLimitInputPlaceholder": "" + }, + "upsellBtnLabel": "" }, "quickSearch": { "placeholder": "" diff --git a/packages/excalidraw/locales/kaa.json b/packages/excalidraw/locales/kaa.json index f224d85c82..3763d9d52d 100644 --- a/packages/excalidraw/locales/kaa.json +++ b/packages/excalidraw/locales/kaa.json @@ -558,6 +558,8 @@ "welcomeScreen": { "app": { "center_heading": "", + "center_heading_line2": "", + "center_heading_line3": "", "center_heading_plus": "", "menuHint": "Eksportlaw, sazlawlar, tiller, ..." }, @@ -612,7 +614,55 @@ "button": "", "description": "", "syntax": "", - "preview": "" + "preview": "", + "label": "", + "inputPlaceholder": "" + }, + "ttd": { + "error": "" + }, + "chat": { + "inputPlaceholder": "", + "inputPlaceholderWithMessages": "", + "generating": "", + "rateLimitRemaining": "", + "role": { + "user": "", + "assistant": "", + "system": "" + }, + "aiBeta": "", + "label": "", + "menu": "", + "newChat": "", + "deleteChat": "", + "deleteMessage": "", + "viewAsMermaid": "", + "placeholder": { + "title": "", + "description": "", + "hint": "" + }, + "preview": "", + "insert": "", + "retry": "", + "errors": { + "promptTooShort": "", + "promptTooLong": "", + "generationFailed": "", + "invalidDiagram": "", + "fixInMermaid": "", + "aiRepair": "", + "requestAborted": "", + "requestFailed": "", + "mermaidParseError": "" + }, + "rateLimit": { + "messageLimit": "", + "generalRateLimit": "", + "messageLimitInputPlaceholder": "" + }, + "upsellBtnLabel": "" }, "quickSearch": { "placeholder": "" diff --git a/packages/excalidraw/locales/kab-KAB.json b/packages/excalidraw/locales/kab-KAB.json index c4ce28dff2..3c2e72b255 100644 --- a/packages/excalidraw/locales/kab-KAB.json +++ b/packages/excalidraw/locales/kab-KAB.json @@ -557,7 +557,9 @@ }, "welcomeScreen": { "app": { - "center_heading": "Akk isefka-inek•inem ttwakelsen s wudem adigan deg yiminig-inek•inem.", + "center_heading": "", + "center_heading_line2": "", + "center_heading_line3": "", "center_heading_plus": "Tebɣiḍ ad tedduḍ ɣer Excalidraw+ deg umḍiq?", "menuHint": "Asifeḍ, ismenyifen, tutlayin, ..." }, @@ -612,7 +614,55 @@ "button": "", "description": "", "syntax": "", - "preview": "" + "preview": "", + "label": "", + "inputPlaceholder": "" + }, + "ttd": { + "error": "" + }, + "chat": { + "inputPlaceholder": "", + "inputPlaceholderWithMessages": "", + "generating": "", + "rateLimitRemaining": "", + "role": { + "user": "", + "assistant": "", + "system": "" + }, + "aiBeta": "", + "label": "", + "menu": "", + "newChat": "", + "deleteChat": "", + "deleteMessage": "", + "viewAsMermaid": "", + "placeholder": { + "title": "", + "description": "", + "hint": "" + }, + "preview": "", + "insert": "", + "retry": "", + "errors": { + "promptTooShort": "", + "promptTooLong": "", + "generationFailed": "", + "invalidDiagram": "", + "fixInMermaid": "", + "aiRepair": "", + "requestAborted": "", + "requestFailed": "", + "mermaidParseError": "" + }, + "rateLimit": { + "messageLimit": "", + "generalRateLimit": "", + "messageLimitInputPlaceholder": "" + }, + "upsellBtnLabel": "" }, "quickSearch": { "placeholder": "" diff --git a/packages/excalidraw/locales/kk-KZ.json b/packages/excalidraw/locales/kk-KZ.json index 4bbf8e870a..eea2b982ef 100644 --- a/packages/excalidraw/locales/kk-KZ.json +++ b/packages/excalidraw/locales/kk-KZ.json @@ -558,6 +558,8 @@ "welcomeScreen": { "app": { "center_heading": "", + "center_heading_line2": "", + "center_heading_line3": "", "center_heading_plus": "", "menuHint": "" }, @@ -612,7 +614,55 @@ "button": "", "description": "", "syntax": "", - "preview": "" + "preview": "", + "label": "", + "inputPlaceholder": "" + }, + "ttd": { + "error": "" + }, + "chat": { + "inputPlaceholder": "", + "inputPlaceholderWithMessages": "", + "generating": "", + "rateLimitRemaining": "", + "role": { + "user": "", + "assistant": "", + "system": "" + }, + "aiBeta": "", + "label": "", + "menu": "", + "newChat": "", + "deleteChat": "", + "deleteMessage": "", + "viewAsMermaid": "", + "placeholder": { + "title": "", + "description": "", + "hint": "" + }, + "preview": "", + "insert": "", + "retry": "", + "errors": { + "promptTooShort": "", + "promptTooLong": "", + "generationFailed": "", + "invalidDiagram": "", + "fixInMermaid": "", + "aiRepair": "", + "requestAborted": "", + "requestFailed": "", + "mermaidParseError": "" + }, + "rateLimit": { + "messageLimit": "", + "generalRateLimit": "", + "messageLimitInputPlaceholder": "" + }, + "upsellBtnLabel": "" }, "quickSearch": { "placeholder": "" diff --git a/packages/excalidraw/locales/km-KH.json b/packages/excalidraw/locales/km-KH.json index 91dbf6dbb6..5a8529a7cf 100644 --- a/packages/excalidraw/locales/km-KH.json +++ b/packages/excalidraw/locales/km-KH.json @@ -557,7 +557,9 @@ }, "welcomeScreen": { "app": { - "center_heading": "ទិន្នន័យទាំងអស់របស់អ្នក ត្រូវបានរក្សាទុកនៅក្នុង browser របស់អ្នក ។", + "center_heading": "", + "center_heading_line2": "", + "center_heading_line3": "", "center_heading_plus": "តើ​អ្នក​ចង់​ទៅ Excalidraw+ ​វិញ ឬ មែន?", "menuHint": "នាំចេញ ចំណូលចិត្ត ភាសា ..." }, @@ -612,7 +614,55 @@ "button": "", "description": "", "syntax": "", - "preview": "" + "preview": "", + "label": "", + "inputPlaceholder": "" + }, + "ttd": { + "error": "" + }, + "chat": { + "inputPlaceholder": "", + "inputPlaceholderWithMessages": "", + "generating": "", + "rateLimitRemaining": "", + "role": { + "user": "", + "assistant": "", + "system": "" + }, + "aiBeta": "", + "label": "", + "menu": "", + "newChat": "", + "deleteChat": "", + "deleteMessage": "", + "viewAsMermaid": "", + "placeholder": { + "title": "", + "description": "", + "hint": "" + }, + "preview": "", + "insert": "", + "retry": "", + "errors": { + "promptTooShort": "", + "promptTooLong": "", + "generationFailed": "", + "invalidDiagram": "", + "fixInMermaid": "", + "aiRepair": "", + "requestAborted": "", + "requestFailed": "", + "mermaidParseError": "" + }, + "rateLimit": { + "messageLimit": "", + "generalRateLimit": "", + "messageLimitInputPlaceholder": "" + }, + "upsellBtnLabel": "" }, "quickSearch": { "placeholder": "" diff --git a/packages/excalidraw/locales/ko-KR.json b/packages/excalidraw/locales/ko-KR.json index 51bba84854..1d3ad461a4 100644 --- a/packages/excalidraw/locales/ko-KR.json +++ b/packages/excalidraw/locales/ko-KR.json @@ -557,7 +557,9 @@ }, "welcomeScreen": { "app": { - "center_heading": "모든 데이터는 브라우저에 안전하게 저장됩니다.", + "center_heading": "", + "center_heading_line2": "", + "center_heading_line3": "", "center_heading_plus": "대신 Excalidraw+로 이동하시겠습니까?", "menuHint": "내보내기, 설정, 언어, ..." }, @@ -612,7 +614,55 @@ "button": "", "description": "", "syntax": "", - "preview": "" + "preview": "", + "label": "", + "inputPlaceholder": "" + }, + "ttd": { + "error": "" + }, + "chat": { + "inputPlaceholder": "", + "inputPlaceholderWithMessages": "", + "generating": "", + "rateLimitRemaining": "", + "role": { + "user": "", + "assistant": "", + "system": "" + }, + "aiBeta": "", + "label": "", + "menu": "", + "newChat": "", + "deleteChat": "", + "deleteMessage": "", + "viewAsMermaid": "", + "placeholder": { + "title": "", + "description": "", + "hint": "" + }, + "preview": "", + "insert": "", + "retry": "", + "errors": { + "promptTooShort": "", + "promptTooLong": "", + "generationFailed": "", + "invalidDiagram": "", + "fixInMermaid": "", + "aiRepair": "", + "requestAborted": "", + "requestFailed": "", + "mermaidParseError": "" + }, + "rateLimit": { + "messageLimit": "", + "generalRateLimit": "", + "messageLimitInputPlaceholder": "" + }, + "upsellBtnLabel": "" }, "quickSearch": { "placeholder": "" diff --git a/packages/excalidraw/locales/ku-TR.json b/packages/excalidraw/locales/ku-TR.json index 2a9c939e7e..aa26ea1ca7 100644 --- a/packages/excalidraw/locales/ku-TR.json +++ b/packages/excalidraw/locales/ku-TR.json @@ -557,7 +557,9 @@ }, "welcomeScreen": { "app": { - "center_heading": "هەموو داتاکانت لە ناوخۆی وێنگەڕەکەتدا پاشەکەوت کراوە.", + "center_heading": "", + "center_heading_line2": "", + "center_heading_line3": "", "center_heading_plus": "ویستت بڕۆیت بۆ Excalidraw+?", "menuHint": "هەناردەکردن، ڕێکخستنەکان، زمانەکان، ..." }, @@ -612,7 +614,55 @@ "button": "", "description": "", "syntax": "", - "preview": "" + "preview": "", + "label": "", + "inputPlaceholder": "" + }, + "ttd": { + "error": "" + }, + "chat": { + "inputPlaceholder": "", + "inputPlaceholderWithMessages": "", + "generating": "", + "rateLimitRemaining": "", + "role": { + "user": "", + "assistant": "", + "system": "" + }, + "aiBeta": "", + "label": "", + "menu": "", + "newChat": "", + "deleteChat": "", + "deleteMessage": "", + "viewAsMermaid": "", + "placeholder": { + "title": "", + "description": "", + "hint": "" + }, + "preview": "", + "insert": "", + "retry": "", + "errors": { + "promptTooShort": "", + "promptTooLong": "", + "generationFailed": "", + "invalidDiagram": "", + "fixInMermaid": "", + "aiRepair": "", + "requestAborted": "", + "requestFailed": "", + "mermaidParseError": "" + }, + "rateLimit": { + "messageLimit": "", + "generalRateLimit": "", + "messageLimitInputPlaceholder": "" + }, + "upsellBtnLabel": "" }, "quickSearch": { "placeholder": "" diff --git a/packages/excalidraw/locales/lt-LT.json b/packages/excalidraw/locales/lt-LT.json index dc78eafcae..2cbefa0984 100644 --- a/packages/excalidraw/locales/lt-LT.json +++ b/packages/excalidraw/locales/lt-LT.json @@ -558,6 +558,8 @@ "welcomeScreen": { "app": { "center_heading": "", + "center_heading_line2": "", + "center_heading_line3": "", "center_heading_plus": "", "menuHint": "" }, @@ -612,7 +614,55 @@ "button": "", "description": "", "syntax": "", - "preview": "" + "preview": "", + "label": "", + "inputPlaceholder": "" + }, + "ttd": { + "error": "" + }, + "chat": { + "inputPlaceholder": "", + "inputPlaceholderWithMessages": "", + "generating": "", + "rateLimitRemaining": "", + "role": { + "user": "", + "assistant": "", + "system": "" + }, + "aiBeta": "", + "label": "", + "menu": "", + "newChat": "", + "deleteChat": "", + "deleteMessage": "", + "viewAsMermaid": "", + "placeholder": { + "title": "", + "description": "", + "hint": "" + }, + "preview": "", + "insert": "", + "retry": "", + "errors": { + "promptTooShort": "", + "promptTooLong": "", + "generationFailed": "", + "invalidDiagram": "", + "fixInMermaid": "", + "aiRepair": "", + "requestAborted": "", + "requestFailed": "", + "mermaidParseError": "" + }, + "rateLimit": { + "messageLimit": "", + "generalRateLimit": "", + "messageLimitInputPlaceholder": "" + }, + "upsellBtnLabel": "" }, "quickSearch": { "placeholder": "" diff --git a/packages/excalidraw/locales/lv-LV.json b/packages/excalidraw/locales/lv-LV.json index 65d0b786b5..7c628e2888 100644 --- a/packages/excalidraw/locales/lv-LV.json +++ b/packages/excalidraw/locales/lv-LV.json @@ -557,7 +557,9 @@ }, "welcomeScreen": { "app": { - "center_heading": "Visi jūsu dati tiek glabāti uz vietas jūsu pārlūkā.", + "center_heading": "", + "center_heading_line2": "", + "center_heading_line3": "", "center_heading_plus": "Vai tā vietā vēlies doties uz Excalidraw+?", "menuHint": "Eksportēšana, iestatījumi, valodas..." }, @@ -612,7 +614,55 @@ "button": "", "description": "", "syntax": "", - "preview": "" + "preview": "", + "label": "", + "inputPlaceholder": "" + }, + "ttd": { + "error": "" + }, + "chat": { + "inputPlaceholder": "", + "inputPlaceholderWithMessages": "", + "generating": "", + "rateLimitRemaining": "", + "role": { + "user": "", + "assistant": "", + "system": "" + }, + "aiBeta": "", + "label": "", + "menu": "", + "newChat": "", + "deleteChat": "", + "deleteMessage": "", + "viewAsMermaid": "", + "placeholder": { + "title": "", + "description": "", + "hint": "" + }, + "preview": "", + "insert": "", + "retry": "", + "errors": { + "promptTooShort": "", + "promptTooLong": "", + "generationFailed": "", + "invalidDiagram": "", + "fixInMermaid": "", + "aiRepair": "", + "requestAborted": "", + "requestFailed": "", + "mermaidParseError": "" + }, + "rateLimit": { + "messageLimit": "", + "generalRateLimit": "", + "messageLimitInputPlaceholder": "" + }, + "upsellBtnLabel": "" }, "quickSearch": { "placeholder": "" diff --git a/packages/excalidraw/locales/mr-IN.json b/packages/excalidraw/locales/mr-IN.json index eb47789013..24f6b15da5 100644 --- a/packages/excalidraw/locales/mr-IN.json +++ b/packages/excalidraw/locales/mr-IN.json @@ -557,7 +557,9 @@ }, "welcomeScreen": { "app": { - "center_heading": "तुमचा सर्व डेटा तुमच्या ब्राउझरमध्ये स्थानिक पातळीवर जतन केला जातो.", + "center_heading": "", + "center_heading_line2": "", + "center_heading_line3": "", "center_heading_plus": "त्याऐवजी तुम्हाला Excalidraw+ वर जायचे आहे का?", "menuHint": "निर्यात, आवड़ी-निवडी, भाषा, ..." }, @@ -612,7 +614,55 @@ "button": "शिरवा", "description": "सध्या फक्त प्रवाह चित्र (फ़्लो चार्ट) आणि क्रम चित्र (सिकवेंस ड़ायग्राम) करता येतात. बाक़ीचे चित्र प्रकार एक्सकाली चित्र पद्धति नी चित्रित होतील.", "syntax": "मर्मेड संरचना नियम", - "preview": "पूर्वावलोकन" + "preview": "पूर्वावलोकन", + "label": "", + "inputPlaceholder": "" + }, + "ttd": { + "error": "" + }, + "chat": { + "inputPlaceholder": "", + "inputPlaceholderWithMessages": "", + "generating": "", + "rateLimitRemaining": "", + "role": { + "user": "", + "assistant": "", + "system": "" + }, + "aiBeta": "", + "label": "", + "menu": "", + "newChat": "", + "deleteChat": "", + "deleteMessage": "", + "viewAsMermaid": "", + "placeholder": { + "title": "", + "description": "", + "hint": "" + }, + "preview": "", + "insert": "", + "retry": "", + "errors": { + "promptTooShort": "", + "promptTooLong": "", + "generationFailed": "", + "invalidDiagram": "", + "fixInMermaid": "", + "aiRepair": "", + "requestAborted": "", + "requestFailed": "", + "mermaidParseError": "" + }, + "rateLimit": { + "messageLimit": "", + "generalRateLimit": "", + "messageLimitInputPlaceholder": "" + }, + "upsellBtnLabel": "" }, "quickSearch": { "placeholder": "जलद शोध" diff --git a/packages/excalidraw/locales/my-MM.json b/packages/excalidraw/locales/my-MM.json index 16526bad99..d7576a1bae 100644 --- a/packages/excalidraw/locales/my-MM.json +++ b/packages/excalidraw/locales/my-MM.json @@ -558,6 +558,8 @@ "welcomeScreen": { "app": { "center_heading": "", + "center_heading_line2": "", + "center_heading_line3": "", "center_heading_plus": "", "menuHint": "" }, @@ -612,7 +614,55 @@ "button": "", "description": "", "syntax": "", - "preview": "" + "preview": "", + "label": "", + "inputPlaceholder": "" + }, + "ttd": { + "error": "" + }, + "chat": { + "inputPlaceholder": "", + "inputPlaceholderWithMessages": "", + "generating": "", + "rateLimitRemaining": "", + "role": { + "user": "", + "assistant": "", + "system": "" + }, + "aiBeta": "", + "label": "", + "menu": "", + "newChat": "", + "deleteChat": "", + "deleteMessage": "", + "viewAsMermaid": "", + "placeholder": { + "title": "", + "description": "", + "hint": "" + }, + "preview": "", + "insert": "", + "retry": "", + "errors": { + "promptTooShort": "", + "promptTooLong": "", + "generationFailed": "", + "invalidDiagram": "", + "fixInMermaid": "", + "aiRepair": "", + "requestAborted": "", + "requestFailed": "", + "mermaidParseError": "" + }, + "rateLimit": { + "messageLimit": "", + "generalRateLimit": "", + "messageLimitInputPlaceholder": "" + }, + "upsellBtnLabel": "" }, "quickSearch": { "placeholder": "" diff --git a/packages/excalidraw/locales/nb-NO.json b/packages/excalidraw/locales/nb-NO.json index 8dc47529ee..4a4fef3f01 100644 --- a/packages/excalidraw/locales/nb-NO.json +++ b/packages/excalidraw/locales/nb-NO.json @@ -557,7 +557,9 @@ }, "welcomeScreen": { "app": { - "center_heading": "Alle dine data lagres lokalt i din nettleser.", + "center_heading": "", + "center_heading_line2": "", + "center_heading_line3": "", "center_heading_plus": "Ønsker du å gå til Excalidraw+ i stedet?", "menuHint": "Eksporter, innstillinger, språk, ..." }, @@ -612,7 +614,55 @@ "button": "Sett inn", "description": "Foreløpig er bare Flowchart-, Sequence- og klasse -diagrammer støttet. De andre typene vil bli gjengitt som bilde i Excalidraw.", "syntax": "Mermaid-syntaks", - "preview": "Forhåndsvisning" + "preview": "Forhåndsvisning", + "label": "", + "inputPlaceholder": "" + }, + "ttd": { + "error": "" + }, + "chat": { + "inputPlaceholder": "", + "inputPlaceholderWithMessages": "", + "generating": "", + "rateLimitRemaining": "", + "role": { + "user": "", + "assistant": "", + "system": "" + }, + "aiBeta": "", + "label": "", + "menu": "", + "newChat": "", + "deleteChat": "", + "deleteMessage": "", + "viewAsMermaid": "", + "placeholder": { + "title": "", + "description": "", + "hint": "" + }, + "preview": "", + "insert": "", + "retry": "", + "errors": { + "promptTooShort": "", + "promptTooLong": "", + "generationFailed": "", + "invalidDiagram": "", + "fixInMermaid": "", + "aiRepair": "", + "requestAborted": "", + "requestFailed": "", + "mermaidParseError": "" + }, + "rateLimit": { + "messageLimit": "", + "generalRateLimit": "", + "messageLimitInputPlaceholder": "" + }, + "upsellBtnLabel": "" }, "quickSearch": { "placeholder": "Hurtigsøk" diff --git a/packages/excalidraw/locales/nl-NL.json b/packages/excalidraw/locales/nl-NL.json index 37bbfcf05d..9c7d292ae3 100644 --- a/packages/excalidraw/locales/nl-NL.json +++ b/packages/excalidraw/locales/nl-NL.json @@ -2,7 +2,7 @@ "labels": { "paste": "Plakken", "pasteAsPlaintext": "Plakken als platte tekst", - "pasteCharts": "Plak grafieken", + "pasteCharts": "Grafieken plakken", "selectAll": "Alles selecteren", "multiSelect": "Voeg element toe aan selectie", "moveCanvas": "Canvas verplaatsen", @@ -557,7 +557,9 @@ }, "welcomeScreen": { "app": { - "center_heading": "Al je data is lokaal opgeslagen in je browser.", + "center_heading": "", + "center_heading_line2": "", + "center_heading_line3": "", "center_heading_plus": "Wil je in plaats daarvan naar Excalidraw+ gaan?", "menuHint": "Exporteren, voorkeuren en meer, ..." }, @@ -612,7 +614,55 @@ "button": "Invoegen", "description": "Momenteel worden alleen Flowchart-, Sequence- en Class-diagrammen ondersteund. De andere types worden als afbeelding weergegeven in Excalidraw.", "syntax": "Mermaid Syntaxis", - "preview": "Voorbeeld" + "preview": "Voorbeeld", + "label": "Mermaid", + "inputPlaceholder": "Noteer Mermaid diagram omschrijving hier..." + }, + "ttd": { + "error": "Fout!" + }, + "chat": { + "inputPlaceholder": "Begin je diagram idee hier in te typen... ({{shortcut}} voor een nieuwe lijn)", + "inputPlaceholderWithMessages": "Ga door met het verfijnen van je diagram...", + "generating": "", + "rateLimitRemaining": "{{count}} verzoeken over vandaag", + "role": { + "user": "Jij", + "assistant": "AI Assistent", + "system": "Systeem" + }, + "aiBeta": "AI Beta", + "label": "Chat", + "menu": "Menu", + "newChat": "Nieuwe Chat", + "deleteChat": "Chat verwijderen", + "deleteMessage": "Bericht verwijderen", + "viewAsMermaid": "Weergeven als Mermaid", + "placeholder": { + "title": "Laten we jouw diagram ontwerpen", + "description": "Beschrijf het diagram dat je wilt aanmaken, en we genereren het voor je.", + "hint": "Op dit moment kennen we Flowchart, Sequence, en Class diagrammen." + }, + "preview": "Voorbeeld", + "insert": "Invoegen", + "retry": "Opnieuw proberen", + "errors": { + "promptTooShort": "Vraag is te kort (min {{min}} karakters)", + "promptTooLong": "Vraag is te kort (max {{max}} karakters)", + "generationFailed": "Generatie mislukt", + "invalidDiagram": "Een ongeldige diagram gegenereerd :(. Je kunt handmatig bewerken, opnieuw proberen met automatisch repareren of een andere vraag proberen.", + "fixInMermaid": "Mermaid handmatig bewerken →", + "aiRepair": "Opnieuw genereren (automatisch repareren) →", + "requestAborted": "Verzoek gestopt", + "requestFailed": "Verzoek mislukt", + "mermaidParseError": "" + }, + "rateLimit": { + "messageLimit": "Je hebt je AI limiet bereikt op het gratis abonnement. Probeer Excalidraw+ uit voor meer of kom morgen terug.", + "generalRateLimit": "Ho ho, rustig aan, je gaat te snel voor ons! Wacht even voor je het opnieuw probeert.", + "messageLimitInputPlaceholder": "Je hebt je berichtenlimiet bereikt" + }, + "upsellBtnLabel": "Upgrade naar Plus" }, "quickSearch": { "placeholder": "Snel Zoeken" diff --git a/packages/excalidraw/locales/nn-NO.json b/packages/excalidraw/locales/nn-NO.json index d57141f93d..2cca629bf4 100644 --- a/packages/excalidraw/locales/nn-NO.json +++ b/packages/excalidraw/locales/nn-NO.json @@ -558,6 +558,8 @@ "welcomeScreen": { "app": { "center_heading": "", + "center_heading_line2": "", + "center_heading_line3": "", "center_heading_plus": "", "menuHint": "" }, @@ -612,7 +614,55 @@ "button": "", "description": "", "syntax": "", - "preview": "" + "preview": "", + "label": "", + "inputPlaceholder": "" + }, + "ttd": { + "error": "" + }, + "chat": { + "inputPlaceholder": "", + "inputPlaceholderWithMessages": "", + "generating": "", + "rateLimitRemaining": "", + "role": { + "user": "", + "assistant": "", + "system": "" + }, + "aiBeta": "", + "label": "", + "menu": "", + "newChat": "", + "deleteChat": "", + "deleteMessage": "", + "viewAsMermaid": "", + "placeholder": { + "title": "", + "description": "", + "hint": "" + }, + "preview": "", + "insert": "", + "retry": "", + "errors": { + "promptTooShort": "", + "promptTooLong": "", + "generationFailed": "", + "invalidDiagram": "", + "fixInMermaid": "", + "aiRepair": "", + "requestAborted": "", + "requestFailed": "", + "mermaidParseError": "" + }, + "rateLimit": { + "messageLimit": "", + "generalRateLimit": "", + "messageLimitInputPlaceholder": "" + }, + "upsellBtnLabel": "" }, "quickSearch": { "placeholder": "" diff --git a/packages/excalidraw/locales/oc-FR.json b/packages/excalidraw/locales/oc-FR.json index db2f43a90d..573b77c1f9 100644 --- a/packages/excalidraw/locales/oc-FR.json +++ b/packages/excalidraw/locales/oc-FR.json @@ -557,7 +557,9 @@ }, "welcomeScreen": { "app": { - "center_heading": "Totas las donadas son enregistradas dins vòstre navegador.", + "center_heading": "", + "center_heading_line2": "", + "center_heading_line3": "", "center_heading_plus": "Voliatz puslèu utilizar Excalidraw+ a la plaça ?", "menuHint": "Exportar, preferéncias, lengas, ..." }, @@ -612,7 +614,55 @@ "button": "Inserir", "description": "Actualament, sonque los diagramas logics, de sequéncia e de classa son preses en carga. Los autres tipes seràn afichats coma imatge dins Excalidraw.", "syntax": "Sintaxi Mermaid", - "preview": "Apercebut" + "preview": "Apercebut", + "label": "", + "inputPlaceholder": "" + }, + "ttd": { + "error": "" + }, + "chat": { + "inputPlaceholder": "", + "inputPlaceholderWithMessages": "", + "generating": "", + "rateLimitRemaining": "", + "role": { + "user": "", + "assistant": "", + "system": "" + }, + "aiBeta": "", + "label": "", + "menu": "", + "newChat": "", + "deleteChat": "", + "deleteMessage": "", + "viewAsMermaid": "", + "placeholder": { + "title": "", + "description": "", + "hint": "" + }, + "preview": "", + "insert": "", + "retry": "", + "errors": { + "promptTooShort": "", + "promptTooLong": "", + "generationFailed": "", + "invalidDiagram": "", + "fixInMermaid": "", + "aiRepair": "", + "requestAborted": "", + "requestFailed": "", + "mermaidParseError": "" + }, + "rateLimit": { + "messageLimit": "", + "generalRateLimit": "", + "messageLimitInputPlaceholder": "" + }, + "upsellBtnLabel": "" }, "quickSearch": { "placeholder": "Recèrca rapida" diff --git a/packages/excalidraw/locales/pa-IN.json b/packages/excalidraw/locales/pa-IN.json index bf20e81cf9..6cceb0f548 100644 --- a/packages/excalidraw/locales/pa-IN.json +++ b/packages/excalidraw/locales/pa-IN.json @@ -557,7 +557,9 @@ }, "welcomeScreen": { "app": { - "center_heading": "ਤੁਹਾਡਾ ਸਾਰਾ ਡਾਟਾ ਤੁਹਾਡੇ ਲੋਕਲ ਬਰਾਉਜ਼ਰ ਵਿੱਚ ਸਾਂਭਿਆ ਹੋਇਆ ਹੈ।", + "center_heading": "", + "center_heading_line2": "", + "center_heading_line3": "", "center_heading_plus": "", "menuHint": "ਨਿਰਯਾਤ, ਤਰਜੀਹਾਂ, ਭਾਸ਼ਾਵਾਂ, …" }, @@ -612,7 +614,55 @@ "button": "", "description": "", "syntax": "", - "preview": "" + "preview": "", + "label": "", + "inputPlaceholder": "" + }, + "ttd": { + "error": "" + }, + "chat": { + "inputPlaceholder": "", + "inputPlaceholderWithMessages": "", + "generating": "", + "rateLimitRemaining": "", + "role": { + "user": "", + "assistant": "", + "system": "" + }, + "aiBeta": "", + "label": "", + "menu": "", + "newChat": "", + "deleteChat": "", + "deleteMessage": "", + "viewAsMermaid": "", + "placeholder": { + "title": "", + "description": "", + "hint": "" + }, + "preview": "", + "insert": "", + "retry": "", + "errors": { + "promptTooShort": "", + "promptTooLong": "", + "generationFailed": "", + "invalidDiagram": "", + "fixInMermaid": "", + "aiRepair": "", + "requestAborted": "", + "requestFailed": "", + "mermaidParseError": "" + }, + "rateLimit": { + "messageLimit": "", + "generalRateLimit": "", + "messageLimitInputPlaceholder": "" + }, + "upsellBtnLabel": "" }, "quickSearch": { "placeholder": "" diff --git a/packages/excalidraw/locales/percentages.json b/packages/excalidraw/locales/percentages.json index 8b961563d8..61fffadd56 100644 --- a/packages/excalidraw/locales/percentages.json +++ b/packages/excalidraw/locales/percentages.json @@ -1,59 +1,59 @@ { - "ar-SA": 91, - "az-AZ": 27, - "bg-BG": 70, - "bn-BD": 42, - "bn-IN": 42, - "ca-ES": 91, - "cs-CZ": 82, - "da-DK": 30, - "de-CH": 91, - "de-DE": 92, - "el-GR": 86, + "ar-SA": 84, + "az-AZ": 25, + "bg-BG": 65, + "bn-BD": 39, + "bn-IN": 39, + "ca-ES": 84, + "cs-CZ": 76, + "da-DK": 28, + "de-CH": 85, + "de-DE": 85, + "el-GR": 80, "en": 100, - "es-ES": 91, - "eu-ES": 78, - "fa-IR": 91, - "fi-FI": 67, - "fr-FR": 99, - "gl-ES": 67, - "he-IL": 91, - "hi-IN": 91, - "hu-HU": 53, - "id-ID": 90, - "it-IT": 100, - "ja-JP": 87, - "kaa": 23, - "kab-KAB": 60, - "kk-KZ": 14, - "km-KH": 60, - "ko-KR": 80, - "ku-TR": 64, - "lt-LT": 37, - "lv-LV": 56, - "mr-IN": 91, - "my-MM": 27, - "nb-NO": 91, - "nl-NL": 100, - "nn-NO": 48, - "oc-FR": 84, - "pa-IN": 58, - "pl-PL": 99, - "pt-BR": 90, - "pt-PT": 91, + "es-ES": 85, + "eu-ES": 72, + "fa-IR": 84, + "fi-FI": 62, + "fr-FR": 92, + "gl-ES": 63, + "he-IL": 84, + "hi-IN": 86, + "hu-HU": 67, + "id-ID": 84, + "it-IT": 99, + "ja-JP": 81, + "kaa": 22, + "kab-KAB": 56, + "kk-KZ": 13, + "km-KH": 56, + "ko-KR": 75, + "ku-TR": 59, + "lt-LT": 35, + "lv-LV": 53, + "mr-IN": 84, + "my-MM": 25, + "nb-NO": 84, + "nl-NL": 99, + "nn-NO": 45, + "oc-FR": 79, + "pa-IN": 54, + "pl-PL": 93, + "pt-BR": 84, + "pt-PT": 84, "ro-RO": 100, "ru-RU": 99, - "si-LK": 75, - "sk-SK": 100, - "sl-SI": 91, - "sv-SE": 91, - "ta-IN": 86, - "th-TH": 63, - "tr-TR": 94, - "uk-UA": 90, + "si-LK": 70, + "sk-SK": 93, + "sl-SI": 84, + "sv-SE": 85, + "ta-IN": 81, + "th-TH": 59, + "tr-TR": 88, + "uk-UA": 84, "uz-UZ": 0, - "vi-VN": 75, - "zh-CN": 99, - "zh-HK": 17, - "zh-TW": 91 + "vi-VN": 70, + "zh-CN": 93, + "zh-HK": 16, + "zh-TW": 84 } diff --git a/packages/excalidraw/locales/pl-PL.json b/packages/excalidraw/locales/pl-PL.json index 8646ebce21..c5ff6777fb 100644 --- a/packages/excalidraw/locales/pl-PL.json +++ b/packages/excalidraw/locales/pl-PL.json @@ -557,7 +557,9 @@ }, "welcomeScreen": { "app": { - "center_heading": "Wszystkie dane są zapisywane lokalnie w przeglądarce.", + "center_heading": "", + "center_heading_line2": "", + "center_heading_line3": "", "center_heading_plus": "Czy zamiast tego chcesz przejść do Excalidraw+?", "menuHint": "Eksportuj, preferencje, języki..." }, @@ -612,7 +614,55 @@ "button": "Wstaw", "description": "Obecnie wspierane są jedynie proste grafy, sekwencje i diagramy klas. Pozostałe typy będą wyświetlane jako obrazy w Excalidraw.", "syntax": "Składnia diagramów Mermaid", - "preview": "Podgląd" + "preview": "Podgląd", + "label": "", + "inputPlaceholder": "" + }, + "ttd": { + "error": "" + }, + "chat": { + "inputPlaceholder": "", + "inputPlaceholderWithMessages": "", + "generating": "", + "rateLimitRemaining": "", + "role": { + "user": "", + "assistant": "", + "system": "" + }, + "aiBeta": "", + "label": "", + "menu": "", + "newChat": "", + "deleteChat": "", + "deleteMessage": "", + "viewAsMermaid": "", + "placeholder": { + "title": "", + "description": "", + "hint": "" + }, + "preview": "", + "insert": "", + "retry": "", + "errors": { + "promptTooShort": "", + "promptTooLong": "", + "generationFailed": "", + "invalidDiagram": "", + "fixInMermaid": "", + "aiRepair": "", + "requestAborted": "", + "requestFailed": "", + "mermaidParseError": "" + }, + "rateLimit": { + "messageLimit": "", + "generalRateLimit": "", + "messageLimitInputPlaceholder": "" + }, + "upsellBtnLabel": "" }, "quickSearch": { "placeholder": "Szybkie wyszukiwanie" diff --git a/packages/excalidraw/locales/pt-BR.json b/packages/excalidraw/locales/pt-BR.json index f063bb4b24..934a4d3307 100644 --- a/packages/excalidraw/locales/pt-BR.json +++ b/packages/excalidraw/locales/pt-BR.json @@ -557,7 +557,9 @@ }, "welcomeScreen": { "app": { - "center_heading": "Todos os dados são salvos localmente no seu navegador.", + "center_heading": "", + "center_heading_line2": "", + "center_heading_line3": "", "center_heading_plus": "Você queria ir para o Excalidraw+ em vez disso?", "menuHint": "Exportar, preferências, idiomas..." }, @@ -612,7 +614,55 @@ "button": "Inserir", "description": "Atualmente apenas os diagramasFlowchartSequência, e Classsão suportados. Os outros tipos serão renderizados como uma imagem no Excalidraw.", "syntax": "Sintaxe em Mermaid", - "preview": "Visualizar" + "preview": "Visualizar", + "label": "", + "inputPlaceholder": "" + }, + "ttd": { + "error": "" + }, + "chat": { + "inputPlaceholder": "", + "inputPlaceholderWithMessages": "", + "generating": "", + "rateLimitRemaining": "", + "role": { + "user": "", + "assistant": "", + "system": "" + }, + "aiBeta": "", + "label": "", + "menu": "", + "newChat": "", + "deleteChat": "", + "deleteMessage": "", + "viewAsMermaid": "", + "placeholder": { + "title": "", + "description": "", + "hint": "" + }, + "preview": "", + "insert": "", + "retry": "", + "errors": { + "promptTooShort": "", + "promptTooLong": "", + "generationFailed": "", + "invalidDiagram": "", + "fixInMermaid": "", + "aiRepair": "", + "requestAborted": "", + "requestFailed": "", + "mermaidParseError": "" + }, + "rateLimit": { + "messageLimit": "", + "generalRateLimit": "", + "messageLimitInputPlaceholder": "" + }, + "upsellBtnLabel": "" }, "quickSearch": { "placeholder": "Busca rápida" diff --git a/packages/excalidraw/locales/pt-PT.json b/packages/excalidraw/locales/pt-PT.json index 09d2e530fe..ec9c3d1bbc 100644 --- a/packages/excalidraw/locales/pt-PT.json +++ b/packages/excalidraw/locales/pt-PT.json @@ -557,7 +557,9 @@ }, "welcomeScreen": { "app": { - "center_heading": "Todos os seus dados são guardados no seu navegador local.", + "center_heading": "", + "center_heading_line2": "", + "center_heading_line3": "", "center_heading_plus": "Queria antes ir para o Excalidraw+?", "menuHint": "Exportar, preferências, idiomas..." }, @@ -612,7 +614,55 @@ "button": "Inserir", "description": "Atualmente apenas são suportados diagramas fluxo, sequência, e classe. Os outros tipos serão renderizados como imagem no Excalidraw.", "syntax": "Sintaxe Mermaid", - "preview": "Pré-visualizar" + "preview": "Pré-visualizar", + "label": "", + "inputPlaceholder": "" + }, + "ttd": { + "error": "" + }, + "chat": { + "inputPlaceholder": "", + "inputPlaceholderWithMessages": "", + "generating": "", + "rateLimitRemaining": "", + "role": { + "user": "", + "assistant": "", + "system": "" + }, + "aiBeta": "", + "label": "", + "menu": "", + "newChat": "", + "deleteChat": "", + "deleteMessage": "", + "viewAsMermaid": "", + "placeholder": { + "title": "", + "description": "", + "hint": "" + }, + "preview": "", + "insert": "", + "retry": "", + "errors": { + "promptTooShort": "", + "promptTooLong": "", + "generationFailed": "", + "invalidDiagram": "", + "fixInMermaid": "", + "aiRepair": "", + "requestAborted": "", + "requestFailed": "", + "mermaidParseError": "" + }, + "rateLimit": { + "messageLimit": "", + "generalRateLimit": "", + "messageLimitInputPlaceholder": "" + }, + "upsellBtnLabel": "" }, "quickSearch": { "placeholder": "Pesquisa rápida" diff --git a/packages/excalidraw/locales/ro-RO.json b/packages/excalidraw/locales/ro-RO.json index 177c563b2a..43fee72900 100644 --- a/packages/excalidraw/locales/ro-RO.json +++ b/packages/excalidraw/locales/ro-RO.json @@ -557,7 +557,9 @@ }, "welcomeScreen": { "app": { - "center_heading": "Toate datele tale sunt salvate local în navigatorul tău.", + "center_heading": "Desenele tale sunt salvate în spațiul de stocare al navigatorului tău.", + "center_heading_line2": "Spațiul de stocare al navigatorului poate fi șters în mod neașteptat.", + "center_heading_line3": "Salvează-ți periodic munca într-un fișier pentru a evita pierderea acesteia.", "center_heading_plus": "Ai vrut să mergi în schimb la Excalidraw+?", "menuHint": "Exportare, preferințe, limbi, ..." }, @@ -612,7 +614,55 @@ "button": "Introducere", "description": "În prezent, numai Organigramele, Diagramele de secvență și Diagramele de clasă sunt acceptate. Celelalte tipuri vor fi redate ca imagine în Excalidraw.", "syntax": "Sintaxă Mermaid", - "preview": "Previzualizare" + "preview": "Previzualizare", + "label": "Mermaid", + "inputPlaceholder": "Scrie definiţia diagramei Mermaid aici..." + }, + "ttd": { + "error": "Eroare!" + }, + "chat": { + "inputPlaceholder": "Începe să îți introduci ideea de diagramă aici... ({{shortcut}} pentru linia nouă)", + "inputPlaceholderWithMessages": "Continuă rafinarea diagramei...", + "generating": "Se generează...", + "rateLimitRemaining": "Cereri rămase astăzi: {{count}}", + "role": { + "user": "Tu", + "assistant": "Asistent IA", + "system": "Sistem" + }, + "aiBeta": "Beta IA", + "label": "Conversație", + "menu": "Meniu", + "newChat": "Conversație nouă", + "deleteChat": "Ștergere conversație", + "deleteMessage": "Ștergere mesaj", + "viewAsMermaid": "Vizualizare ca Mermaid", + "placeholder": { + "title": "Hai să-ți proiectăm diagrama", + "description": "Descrie diagrama pe care vrei să o creezi și o vom genera pentru tine.", + "hint": "În acest moment cunoaștem organigrame, diagrame de secvență și diagrame de clasă." + }, + "preview": "Previzualizare", + "insert": "Introducere", + "retry": "Reîncercare", + "errors": { + "promptTooShort": "Solicitarea este prea scurtă (min. {{min}} caractere)", + "promptTooLong": "Solicitarea este prea lungă (max. {{max}} caractere)", + "generationFailed": "Generare nereușită", + "invalidDiagram": "A generat o diagramă nevalidă :(. Poți edita manual, reîncerca cu funcția de fixare automată sau încerca o altă solicitare.", + "fixInMermaid": "Editare Mermaid manual →", + "aiRepair": "Regenerare (fixare automată) →", + "requestAborted": "Cerere anulată", + "requestFailed": "Cerere nereușită", + "mermaidParseError": "Eroare de sintaxă Mermaid" + }, + "rateLimit": { + "messageLimit": "Ai atins limita IA la planul gratuit. Încearcă Excalidraw+ pentru mai multe sau revino mâine.", + "generalRateLimit": "Stai puțin, te grăbești! Așteaptă un moment înainte de a reîncerca.", + "messageLimitInputPlaceholder": "Ai atins limita de mesaje" + }, + "upsellBtnLabel": "Actualizare la Plus" }, "quickSearch": { "placeholder": "Căutare rapidă" diff --git a/packages/excalidraw/locales/ru-RU.json b/packages/excalidraw/locales/ru-RU.json index 17959f4b5f..e2fcb48e2c 100644 --- a/packages/excalidraw/locales/ru-RU.json +++ b/packages/excalidraw/locales/ru-RU.json @@ -38,7 +38,7 @@ "round": "Скруглённые", "arrowheads": "Стрелка", "arrowhead_none": "Нет", - "arrowhead_arrow": "Cтрелка", + "arrowhead_arrow": "Стрелка", "arrowhead_bar": "Черта", "arrowhead_circle": "Круг", "arrowhead_circle_outline": "Круг (контур)", @@ -341,7 +341,7 @@ "canvasPanning": "Чтобы переместить холст, удерживайте {{shortcut_1}} или {{shortcut_2}} во время перетаскивания или используйте инструмент «Рука»", "linearElement": "Нажмите, чтобы начать несколько точек, перетащите для одной линии", "arrowTool": "Нажмите, чтобы начать рисование нескольких точек, перетащите для одной линии. Нажмите {{shortcut}} снова, чтобы изменить тип стрелки.", - "arrowBindModifiers": "", + "arrowBindModifiers": "Удерживайте {{shortcut_1}} , чтобы отключить привязку, или {{shortcut_2}} для привязки в фиксированной точке", "freeDraw": "Нажмите и перетаскивайте, отпустите по завершении", "text": "Совет: при выбранном инструменте выделения дважды щёлкните в любом месте, чтобы добавить текст", "embeddable": "Перетащите для встраивания веб-сайта", @@ -557,7 +557,9 @@ }, "welcomeScreen": { "app": { - "center_heading": "Все ваши данные сохраняются локально в вашем браузере.", + "center_heading": "", + "center_heading_line2": "", + "center_heading_line3": "", "center_heading_plus": "Хотите перейти на Excalidraw+?", "menuHint": "Экспорт, настройки, языки, ..." }, @@ -612,7 +614,55 @@ "button": "Вставить", "description": "В настоящее время поддерживаются только блок-схемы, диаграммы последовательности и диаграммы классов. Другие типы будут отображаться в виде изображения в Excalidraw.", "syntax": "Синтаксис Mermaid", - "preview": "Предпросмотр" + "preview": "Предпросмотр", + "label": "Mermaid", + "inputPlaceholder": "Напишите здесь определение диаграммы Mermaid..." + }, + "ttd": { + "error": "Ошибка!" + }, + "chat": { + "inputPlaceholder": "Начните вводить сюда идею диаграммы... ({{shortcut}} для новой строки)", + "inputPlaceholderWithMessages": "Продолжайте доработку диаграммы...", + "generating": "", + "rateLimitRemaining": "Осталось запросов сегодня: {{count}}", + "role": { + "user": "Вы", + "assistant": "ИИ-помощник", + "system": "Система" + }, + "aiBeta": "ИИ-бета", + "label": "Чат", + "menu": "Меню", + "newChat": "Новый чат", + "deleteChat": "Удалить чат", + "deleteMessage": "Удалить сообщение", + "viewAsMermaid": "Просмотреть как Mermaid", + "placeholder": { + "title": "Давайте создадим вашу диаграмму", + "description": "Опишите диаграмму, которую вы хотите создать, и мы сгенерируем её для вас.", + "hint": "На данный момент мы знаем диаграммы блок-схемы, последовательности и классов." + }, + "preview": "Предпросмотр", + "insert": "Вставить", + "retry": "Повторить", + "errors": { + "promptTooShort": "Подсказка слишком короткая (мин. символов: {{min}})", + "promptTooLong": "Подсказка слишком длинная (макс. символов: {{max}})", + "generationFailed": "Генерация не удалась", + "invalidDiagram": "Сгенерирована неверная диаграмма :(. Вы можете редактировать вручную, повторите с автоисправлением или попробуйте другую подсказку.", + "fixInMermaid": "Изменить Mermaid вручную →", + "aiRepair": "Перегенерировать (автоисправление) →", + "requestAborted": "Запрос отменён", + "requestFailed": "Запрос не удался", + "mermaidParseError": "" + }, + "rateLimit": { + "messageLimit": "Вы достигли лимита ИИ на бесплатном тарифном плане. Попробуйте Excalidraw+ или вернитесь завтра.", + "generalRateLimit": "Подождите, вы слишком быстры для нас! Пожалуйста, подождите немного, прежде чем повторить попытку.", + "messageLimitInputPlaceholder": "Вы достигли лимита сообщений" + }, + "upsellBtnLabel": "Перейти на Plus" }, "quickSearch": { "placeholder": "Быстрый поиск" diff --git a/packages/excalidraw/locales/si-LK.json b/packages/excalidraw/locales/si-LK.json index 51dfaf5bc1..5d3ef161cd 100644 --- a/packages/excalidraw/locales/si-LK.json +++ b/packages/excalidraw/locales/si-LK.json @@ -557,7 +557,9 @@ }, "welcomeScreen": { "app": { - "center_heading": "අයදුම් මධ්‍ය ශීර්ෂය", + "center_heading": "", + "center_heading_line2": "", + "center_heading_line3": "", "center_heading_plus": "අයදුම් මධ්‍ය ශීර්ෂය Plus", "menuHint": "මෙනු ඉඟිය" }, @@ -612,7 +614,55 @@ "button": "Mermaid බොත්තම", "description": "Mermaid විස්තර", "syntax": "Mermaid වාක්‍ය ඛණ්ඩය", - "preview": "Mermaid පූර්වදර්ශනය" + "preview": "Mermaid පූර්වදර්ශනය", + "label": "", + "inputPlaceholder": "" + }, + "ttd": { + "error": "" + }, + "chat": { + "inputPlaceholder": "", + "inputPlaceholderWithMessages": "", + "generating": "", + "rateLimitRemaining": "", + "role": { + "user": "", + "assistant": "", + "system": "" + }, + "aiBeta": "", + "label": "", + "menu": "", + "newChat": "", + "deleteChat": "", + "deleteMessage": "", + "viewAsMermaid": "", + "placeholder": { + "title": "", + "description": "", + "hint": "" + }, + "preview": "", + "insert": "", + "retry": "", + "errors": { + "promptTooShort": "", + "promptTooLong": "", + "generationFailed": "", + "invalidDiagram": "", + "fixInMermaid": "", + "aiRepair": "", + "requestAborted": "", + "requestFailed": "", + "mermaidParseError": "" + }, + "rateLimit": { + "messageLimit": "", + "generalRateLimit": "", + "messageLimitInputPlaceholder": "" + }, + "upsellBtnLabel": "" }, "quickSearch": { "placeholder": "" diff --git a/packages/excalidraw/locales/sk-SK.json b/packages/excalidraw/locales/sk-SK.json index c538cf222b..b73f238a23 100644 --- a/packages/excalidraw/locales/sk-SK.json +++ b/packages/excalidraw/locales/sk-SK.json @@ -557,7 +557,9 @@ }, "welcomeScreen": { "app": { - "center_heading": "Všetky vaše dáta sú uložené lokálne vo vašom prehliadači.", + "center_heading": "", + "center_heading_line2": "", + "center_heading_line3": "", "center_heading_plus": "Chceli ste namiesto toho prejsť do Excalidraw+?", "menuHint": "Exportovanie, nastavenia, jazyky, ..." }, @@ -612,7 +614,55 @@ "button": "Vložiť", "description": "Aktuálne sú podporované iba vývojové diagramy, sekvenčné diagramy a diagramy tried. Ostatné typy budú v Excalidraw vykreslené ako obrázky.", "syntax": "Mermaid syntax", - "preview": "Ukážka" + "preview": "Ukážka", + "label": "", + "inputPlaceholder": "" + }, + "ttd": { + "error": "" + }, + "chat": { + "inputPlaceholder": "", + "inputPlaceholderWithMessages": "", + "generating": "", + "rateLimitRemaining": "", + "role": { + "user": "", + "assistant": "", + "system": "" + }, + "aiBeta": "", + "label": "", + "menu": "", + "newChat": "", + "deleteChat": "", + "deleteMessage": "", + "viewAsMermaid": "", + "placeholder": { + "title": "", + "description": "", + "hint": "" + }, + "preview": "", + "insert": "", + "retry": "", + "errors": { + "promptTooShort": "", + "promptTooLong": "", + "generationFailed": "", + "invalidDiagram": "", + "fixInMermaid": "", + "aiRepair": "", + "requestAborted": "", + "requestFailed": "", + "mermaidParseError": "" + }, + "rateLimit": { + "messageLimit": "", + "generalRateLimit": "", + "messageLimitInputPlaceholder": "" + }, + "upsellBtnLabel": "" }, "quickSearch": { "placeholder": "Rýchle vyhľadávanie" diff --git a/packages/excalidraw/locales/sl-SI.json b/packages/excalidraw/locales/sl-SI.json index 27bfab060f..ccb9a6eed6 100644 --- a/packages/excalidraw/locales/sl-SI.json +++ b/packages/excalidraw/locales/sl-SI.json @@ -557,7 +557,9 @@ }, "welcomeScreen": { "app": { - "center_heading": "Vsi vaši podatki so shranjeni lokalno v vašem brskalniku.", + "center_heading": "", + "center_heading_line2": "", + "center_heading_line3": "", "center_heading_plus": "Ste namesto tega želeli odpreti Excalidraw+?", "menuHint": "Izvoz, nastavitve, jeziki, ..." }, @@ -612,7 +614,55 @@ "button": "Vstavi", "description": "Trenutno so podprti samo diagrami poteka, diagrami zaporedij in Razredni diagrami. Druge vrste bodo upodobljene kot slike v Excalidraw.", "syntax": "Sintaksa Mermaid", - "preview": "Predogled" + "preview": "Predogled", + "label": "", + "inputPlaceholder": "" + }, + "ttd": { + "error": "" + }, + "chat": { + "inputPlaceholder": "", + "inputPlaceholderWithMessages": "", + "generating": "", + "rateLimitRemaining": "", + "role": { + "user": "", + "assistant": "", + "system": "" + }, + "aiBeta": "", + "label": "", + "menu": "", + "newChat": "", + "deleteChat": "", + "deleteMessage": "", + "viewAsMermaid": "", + "placeholder": { + "title": "", + "description": "", + "hint": "" + }, + "preview": "", + "insert": "", + "retry": "", + "errors": { + "promptTooShort": "", + "promptTooLong": "", + "generationFailed": "", + "invalidDiagram": "", + "fixInMermaid": "", + "aiRepair": "", + "requestAborted": "", + "requestFailed": "", + "mermaidParseError": "" + }, + "rateLimit": { + "messageLimit": "", + "generalRateLimit": "", + "messageLimitInputPlaceholder": "" + }, + "upsellBtnLabel": "" }, "quickSearch": { "placeholder": "Hitro iskanje" diff --git a/packages/excalidraw/locales/sv-SE.json b/packages/excalidraw/locales/sv-SE.json index 3c02bd11e0..7834769555 100644 --- a/packages/excalidraw/locales/sv-SE.json +++ b/packages/excalidraw/locales/sv-SE.json @@ -557,7 +557,9 @@ }, "welcomeScreen": { "app": { - "center_heading": "All data sparas lokalt i din webbläsare.", + "center_heading": "", + "center_heading_line2": "", + "center_heading_line3": "", "center_heading_plus": "Ville du gå till Excalidraw+ istället?", "menuHint": "Exportera, inställningar, språk, ..." }, @@ -612,7 +614,55 @@ "button": "Infoga", "description": "För närvarande stöds endast Flödesdiagram, Sekvensdiagram och Klassdiagram. De andra typerna kommer att återges som bild i Excalidraw.", "syntax": "Mermaid-syntax", - "preview": "Förhandsgranska" + "preview": "Förhandsgranska", + "label": "", + "inputPlaceholder": "" + }, + "ttd": { + "error": "" + }, + "chat": { + "inputPlaceholder": "", + "inputPlaceholderWithMessages": "", + "generating": "", + "rateLimitRemaining": "", + "role": { + "user": "", + "assistant": "", + "system": "" + }, + "aiBeta": "", + "label": "", + "menu": "", + "newChat": "", + "deleteChat": "", + "deleteMessage": "", + "viewAsMermaid": "", + "placeholder": { + "title": "", + "description": "", + "hint": "" + }, + "preview": "", + "insert": "", + "retry": "", + "errors": { + "promptTooShort": "", + "promptTooLong": "", + "generationFailed": "", + "invalidDiagram": "", + "fixInMermaid": "", + "aiRepair": "", + "requestAborted": "", + "requestFailed": "", + "mermaidParseError": "" + }, + "rateLimit": { + "messageLimit": "", + "generalRateLimit": "", + "messageLimitInputPlaceholder": "" + }, + "upsellBtnLabel": "" }, "quickSearch": { "placeholder": "Snabbsök" diff --git a/packages/excalidraw/locales/ta-IN.json b/packages/excalidraw/locales/ta-IN.json index 06e5871a2a..d1d4db0965 100644 --- a/packages/excalidraw/locales/ta-IN.json +++ b/packages/excalidraw/locales/ta-IN.json @@ -558,6 +558,8 @@ "welcomeScreen": { "app": { "center_heading": "", + "center_heading_line2": "", + "center_heading_line3": "", "center_heading_plus": "நீங்கள் Excalidraw+க்கு செல்ல விரும்புகிறீர்களா?", "menuHint": "விருப்பத்தேர்வுகள், மொழிகள் போன்றனவை ஏற்றுமதிசெய்..." }, @@ -612,7 +614,55 @@ "button": "புகுத்து", "description": "", "syntax": "மெர்மெய்டு தொடரியல்", - "preview": "முன்னோட்டம்" + "preview": "முன்னோட்டம்", + "label": "", + "inputPlaceholder": "" + }, + "ttd": { + "error": "" + }, + "chat": { + "inputPlaceholder": "", + "inputPlaceholderWithMessages": "", + "generating": "", + "rateLimitRemaining": "", + "role": { + "user": "", + "assistant": "", + "system": "" + }, + "aiBeta": "", + "label": "", + "menu": "", + "newChat": "", + "deleteChat": "", + "deleteMessage": "", + "viewAsMermaid": "", + "placeholder": { + "title": "", + "description": "", + "hint": "" + }, + "preview": "", + "insert": "", + "retry": "", + "errors": { + "promptTooShort": "", + "promptTooLong": "", + "generationFailed": "", + "invalidDiagram": "", + "fixInMermaid": "", + "aiRepair": "", + "requestAborted": "", + "requestFailed": "", + "mermaidParseError": "" + }, + "rateLimit": { + "messageLimit": "", + "generalRateLimit": "", + "messageLimitInputPlaceholder": "" + }, + "upsellBtnLabel": "" }, "quickSearch": { "placeholder": "விரைவு தேடல்" diff --git a/packages/excalidraw/locales/th-TH.json b/packages/excalidraw/locales/th-TH.json index 8dbd1ff778..9de713ce02 100644 --- a/packages/excalidraw/locales/th-TH.json +++ b/packages/excalidraw/locales/th-TH.json @@ -558,6 +558,8 @@ "welcomeScreen": { "app": { "center_heading": "", + "center_heading_line2": "", + "center_heading_line3": "", "center_heading_plus": "", "menuHint": "" }, @@ -612,7 +614,55 @@ "button": "แทรก", "description": "", "syntax": "", - "preview": "ดูตัวอย่าง" + "preview": "ดูตัวอย่าง", + "label": "", + "inputPlaceholder": "" + }, + "ttd": { + "error": "" + }, + "chat": { + "inputPlaceholder": "", + "inputPlaceholderWithMessages": "", + "generating": "", + "rateLimitRemaining": "", + "role": { + "user": "", + "assistant": "", + "system": "" + }, + "aiBeta": "", + "label": "", + "menu": "", + "newChat": "", + "deleteChat": "", + "deleteMessage": "", + "viewAsMermaid": "", + "placeholder": { + "title": "", + "description": "", + "hint": "" + }, + "preview": "", + "insert": "", + "retry": "", + "errors": { + "promptTooShort": "", + "promptTooLong": "", + "generationFailed": "", + "invalidDiagram": "", + "fixInMermaid": "", + "aiRepair": "", + "requestAborted": "", + "requestFailed": "", + "mermaidParseError": "" + }, + "rateLimit": { + "messageLimit": "", + "generalRateLimit": "", + "messageLimitInputPlaceholder": "" + }, + "upsellBtnLabel": "" }, "quickSearch": { "placeholder": "ค้นหาด่วน" diff --git a/packages/excalidraw/locales/tr-TR.json b/packages/excalidraw/locales/tr-TR.json index a0046c3b6c..ef2dbd108c 100644 --- a/packages/excalidraw/locales/tr-TR.json +++ b/packages/excalidraw/locales/tr-TR.json @@ -557,7 +557,9 @@ }, "welcomeScreen": { "app": { - "center_heading": "Tüm verileriniz tarayıcınızda yerel olarak kaydedilir.", + "center_heading": "", + "center_heading_line2": "", + "center_heading_line3": "", "center_heading_plus": "Ecalidraw+'a mı gitmek istediniz?", "menuHint": "Dışa aktar, seçenekler, diller, ..." }, @@ -612,7 +614,55 @@ "button": "Ekle", "description": "Şu anda yalnızca Akış şeması, Dizi, ve Sınıf Diyagramları deskteklenmektedir. Diğer türler, Excalidraw'da görsel olarak çizilecektir.", "syntax": "Mermaid Sözdizimi", - "preview": "Önizleme" + "preview": "Önizleme", + "label": "", + "inputPlaceholder": "" + }, + "ttd": { + "error": "" + }, + "chat": { + "inputPlaceholder": "", + "inputPlaceholderWithMessages": "", + "generating": "", + "rateLimitRemaining": "", + "role": { + "user": "", + "assistant": "", + "system": "" + }, + "aiBeta": "", + "label": "", + "menu": "", + "newChat": "", + "deleteChat": "", + "deleteMessage": "", + "viewAsMermaid": "", + "placeholder": { + "title": "", + "description": "", + "hint": "" + }, + "preview": "", + "insert": "", + "retry": "", + "errors": { + "promptTooShort": "", + "promptTooLong": "", + "generationFailed": "", + "invalidDiagram": "", + "fixInMermaid": "", + "aiRepair": "", + "requestAborted": "", + "requestFailed": "", + "mermaidParseError": "" + }, + "rateLimit": { + "messageLimit": "", + "generalRateLimit": "", + "messageLimitInputPlaceholder": "" + }, + "upsellBtnLabel": "" }, "quickSearch": { "placeholder": "Hızlı arama" diff --git a/packages/excalidraw/locales/uk-UA.json b/packages/excalidraw/locales/uk-UA.json index a5240ad82f..023a9188d6 100644 --- a/packages/excalidraw/locales/uk-UA.json +++ b/packages/excalidraw/locales/uk-UA.json @@ -557,7 +557,9 @@ }, "welcomeScreen": { "app": { - "center_heading": "Всі ваші дані збережено локально у Вашому браузері.", + "center_heading": "", + "center_heading_line2": "", + "center_heading_line3": "", "center_heading_plus": "Чи бажаєте перейти до Excalidraw+?", "menuHint": "Експорт, налаштування, мови, ..." }, @@ -612,7 +614,55 @@ "button": "Вставити", "description": "Наразі підтримується тільки блок-схемидіаграми послідовностей та діаграми класів. Інші типи будуть відображатися як зображення в Excalidraw.", "syntax": "Синтаксис Mermaid", - "preview": "Попередній перегляд" + "preview": "Попередній перегляд", + "label": "", + "inputPlaceholder": "" + }, + "ttd": { + "error": "" + }, + "chat": { + "inputPlaceholder": "", + "inputPlaceholderWithMessages": "", + "generating": "", + "rateLimitRemaining": "", + "role": { + "user": "", + "assistant": "", + "system": "" + }, + "aiBeta": "", + "label": "", + "menu": "", + "newChat": "", + "deleteChat": "", + "deleteMessage": "", + "viewAsMermaid": "", + "placeholder": { + "title": "", + "description": "", + "hint": "" + }, + "preview": "", + "insert": "", + "retry": "", + "errors": { + "promptTooShort": "", + "promptTooLong": "", + "generationFailed": "", + "invalidDiagram": "", + "fixInMermaid": "", + "aiRepair": "", + "requestAborted": "", + "requestFailed": "", + "mermaidParseError": "" + }, + "rateLimit": { + "messageLimit": "", + "generalRateLimit": "", + "messageLimitInputPlaceholder": "" + }, + "upsellBtnLabel": "" }, "quickSearch": { "placeholder": "Швидкий пошук" diff --git a/packages/excalidraw/locales/uz-UZ.json b/packages/excalidraw/locales/uz-UZ.json index 9b1095f669..f06fd97c1f 100644 --- a/packages/excalidraw/locales/uz-UZ.json +++ b/packages/excalidraw/locales/uz-UZ.json @@ -558,6 +558,8 @@ "welcomeScreen": { "app": { "center_heading": "", + "center_heading_line2": "", + "center_heading_line3": "", "center_heading_plus": "", "menuHint": "" }, @@ -612,7 +614,55 @@ "button": "", "description": "", "syntax": "", - "preview": "" + "preview": "", + "label": "", + "inputPlaceholder": "" + }, + "ttd": { + "error": "" + }, + "chat": { + "inputPlaceholder": "", + "inputPlaceholderWithMessages": "", + "generating": "", + "rateLimitRemaining": "", + "role": { + "user": "", + "assistant": "", + "system": "" + }, + "aiBeta": "", + "label": "", + "menu": "", + "newChat": "", + "deleteChat": "", + "deleteMessage": "", + "viewAsMermaid": "", + "placeholder": { + "title": "", + "description": "", + "hint": "" + }, + "preview": "", + "insert": "", + "retry": "", + "errors": { + "promptTooShort": "", + "promptTooLong": "", + "generationFailed": "", + "invalidDiagram": "", + "fixInMermaid": "", + "aiRepair": "", + "requestAborted": "", + "requestFailed": "", + "mermaidParseError": "" + }, + "rateLimit": { + "messageLimit": "", + "generalRateLimit": "", + "messageLimitInputPlaceholder": "" + }, + "upsellBtnLabel": "" }, "quickSearch": { "placeholder": "" diff --git a/packages/excalidraw/locales/vi-VN.json b/packages/excalidraw/locales/vi-VN.json index e83afbf328..7f62efb2b4 100644 --- a/packages/excalidraw/locales/vi-VN.json +++ b/packages/excalidraw/locales/vi-VN.json @@ -558,6 +558,8 @@ "welcomeScreen": { "app": { "center_heading": "", + "center_heading_line2": "", + "center_heading_line3": "", "center_heading_plus": "", "menuHint": "" }, @@ -612,7 +614,55 @@ "button": "", "description": "", "syntax": "", - "preview": "" + "preview": "", + "label": "", + "inputPlaceholder": "" + }, + "ttd": { + "error": "" + }, + "chat": { + "inputPlaceholder": "", + "inputPlaceholderWithMessages": "", + "generating": "", + "rateLimitRemaining": "", + "role": { + "user": "", + "assistant": "", + "system": "" + }, + "aiBeta": "", + "label": "", + "menu": "", + "newChat": "", + "deleteChat": "", + "deleteMessage": "", + "viewAsMermaid": "", + "placeholder": { + "title": "", + "description": "", + "hint": "" + }, + "preview": "", + "insert": "", + "retry": "", + "errors": { + "promptTooShort": "", + "promptTooLong": "", + "generationFailed": "", + "invalidDiagram": "", + "fixInMermaid": "", + "aiRepair": "", + "requestAborted": "", + "requestFailed": "", + "mermaidParseError": "" + }, + "rateLimit": { + "messageLimit": "", + "generalRateLimit": "", + "messageLimitInputPlaceholder": "" + }, + "upsellBtnLabel": "" }, "quickSearch": { "placeholder": "" diff --git a/packages/excalidraw/locales/zh-CN.json b/packages/excalidraw/locales/zh-CN.json index 4f4ff616a8..1658015e9d 100644 --- a/packages/excalidraw/locales/zh-CN.json +++ b/packages/excalidraw/locales/zh-CN.json @@ -557,7 +557,9 @@ }, "welcomeScreen": { "app": { - "center_heading": "您的所有数据都储存在浏览器本地。", + "center_heading": "", + "center_heading_line2": "", + "center_heading_line3": "", "center_heading_plus": "是否前往 Excalidraw+ ?", "menuHint": "导出、首选项、语言……" }, @@ -612,7 +614,55 @@ "button": "插入", "description": "目前仅支持流程图序列图类图。其他类型在 Excalidraw 中将以图像呈现。", "syntax": "Mermaid 语法", - "preview": "预览" + "preview": "预览", + "label": "", + "inputPlaceholder": "" + }, + "ttd": { + "error": "" + }, + "chat": { + "inputPlaceholder": "", + "inputPlaceholderWithMessages": "", + "generating": "", + "rateLimitRemaining": "", + "role": { + "user": "", + "assistant": "", + "system": "" + }, + "aiBeta": "", + "label": "", + "menu": "", + "newChat": "", + "deleteChat": "", + "deleteMessage": "", + "viewAsMermaid": "", + "placeholder": { + "title": "", + "description": "", + "hint": "" + }, + "preview": "", + "insert": "", + "retry": "", + "errors": { + "promptTooShort": "", + "promptTooLong": "", + "generationFailed": "", + "invalidDiagram": "", + "fixInMermaid": "", + "aiRepair": "", + "requestAborted": "", + "requestFailed": "", + "mermaidParseError": "" + }, + "rateLimit": { + "messageLimit": "", + "generalRateLimit": "", + "messageLimitInputPlaceholder": "" + }, + "upsellBtnLabel": "" }, "quickSearch": { "placeholder": "快速搜索" diff --git a/packages/excalidraw/locales/zh-HK.json b/packages/excalidraw/locales/zh-HK.json index 048be6c8d5..880c75a3a8 100644 --- a/packages/excalidraw/locales/zh-HK.json +++ b/packages/excalidraw/locales/zh-HK.json @@ -558,6 +558,8 @@ "welcomeScreen": { "app": { "center_heading": "", + "center_heading_line2": "", + "center_heading_line3": "", "center_heading_plus": "", "menuHint": "" }, @@ -612,7 +614,55 @@ "button": "", "description": "", "syntax": "", - "preview": "" + "preview": "", + "label": "", + "inputPlaceholder": "" + }, + "ttd": { + "error": "" + }, + "chat": { + "inputPlaceholder": "", + "inputPlaceholderWithMessages": "", + "generating": "", + "rateLimitRemaining": "", + "role": { + "user": "", + "assistant": "", + "system": "" + }, + "aiBeta": "", + "label": "", + "menu": "", + "newChat": "", + "deleteChat": "", + "deleteMessage": "", + "viewAsMermaid": "", + "placeholder": { + "title": "", + "description": "", + "hint": "" + }, + "preview": "", + "insert": "", + "retry": "", + "errors": { + "promptTooShort": "", + "promptTooLong": "", + "generationFailed": "", + "invalidDiagram": "", + "fixInMermaid": "", + "aiRepair": "", + "requestAborted": "", + "requestFailed": "", + "mermaidParseError": "" + }, + "rateLimit": { + "messageLimit": "", + "generalRateLimit": "", + "messageLimitInputPlaceholder": "" + }, + "upsellBtnLabel": "" }, "quickSearch": { "placeholder": "" diff --git a/packages/excalidraw/locales/zh-TW.json b/packages/excalidraw/locales/zh-TW.json index 036459bc49..94cdcbf893 100644 --- a/packages/excalidraw/locales/zh-TW.json +++ b/packages/excalidraw/locales/zh-TW.json @@ -557,7 +557,9 @@ }, "welcomeScreen": { "app": { - "center_heading": "所有資料皆已在瀏覽器中儲存於本機", + "center_heading": "", + "center_heading_line2": "", + "center_heading_line3": "", "center_heading_plus": "您是否是要前往 Excalidraw+ ?", "menuHint": "輸出、偏好設定、語言..." }, @@ -612,7 +614,55 @@ "button": "插入", "description": "目前僅支援 FlowchartSequenceClass 圖表。其餘檔案類型在 Excalidraw 將會以圖像呈現。", "syntax": "Mermaid 語法", - "preview": "預覽" + "preview": "預覽", + "label": "", + "inputPlaceholder": "" + }, + "ttd": { + "error": "" + }, + "chat": { + "inputPlaceholder": "", + "inputPlaceholderWithMessages": "", + "generating": "", + "rateLimitRemaining": "", + "role": { + "user": "", + "assistant": "", + "system": "" + }, + "aiBeta": "", + "label": "", + "menu": "", + "newChat": "", + "deleteChat": "", + "deleteMessage": "", + "viewAsMermaid": "", + "placeholder": { + "title": "", + "description": "", + "hint": "" + }, + "preview": "", + "insert": "", + "retry": "", + "errors": { + "promptTooShort": "", + "promptTooLong": "", + "generationFailed": "", + "invalidDiagram": "", + "fixInMermaid": "", + "aiRepair": "", + "requestAborted": "", + "requestFailed": "", + "mermaidParseError": "" + }, + "rateLimit": { + "messageLimit": "", + "generalRateLimit": "", + "messageLimitInputPlaceholder": "" + }, + "upsellBtnLabel": "" }, "quickSearch": { "placeholder": "快速搜尋" From b57f3e009694acaa73e83ab9bab9a1470dd52ab3 Mon Sep 17 00:00:00 2001 From: David Luzar <5153846+dwelle@users.noreply.github.com> Date: Sun, 1 Feb 2026 09:21:30 +0100 Subject: [PATCH 13/32] fix(editor): image positioning in crop editor (#10726) --- packages/excalidraw/components/App.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index e224ca97db..bbd748d8a8 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -648,7 +648,10 @@ class App extends React.Component { lastPointerUpEvent: React.PointerEvent | PointerEvent | null = null; lastPointerMoveEvent: PointerEvent | null = null; + /** current frame pointer cords */ lastPointerMoveCoords: { x: number; y: number } | null = null; + /** previous frame pointer coords */ + previousPointerMoveCoords: { x: number; y: number } | null = null; lastViewportPosition = { x: 0, y: 0 }; animationFrameHandler = new AnimationFrameHandler(); @@ -9020,8 +9023,8 @@ class App extends React.Component { } const lastPointerCoords = - this.lastPointerMoveCoords ?? pointerDownState.origin; - this.lastPointerMoveCoords = pointerCoords; + this.previousPointerMoveCoords ?? pointerDownState.origin; + this.previousPointerMoveCoords = pointerCoords; // We need to initialize dragOffsetXY only after we've updated // `state.selectedElementIds` on pointerDown. Doing it here in pointerMove @@ -9388,13 +9391,13 @@ class App extends React.Component { const nextCrop = { ...crop, x: clamp( - crop.x + + crop.x - offsetVector[0] * Math.sign(croppingElement.scale[0]), 0, image.naturalWidth - crop.width, ), y: clamp( - crop.y + + crop.y - offsetVector[1] * Math.sign(croppingElement.scale[1]), 0, image.naturalHeight - crop.height, @@ -9887,6 +9890,7 @@ class App extends React.Component { // just in case, tool changes mid drag, always clean up this.lassoTrail.endPath(); + this.previousPointerMoveCoords = null; SnapCache.setReferenceSnapPoints(null); SnapCache.setVisibleGaps(null); From d29fd62e417b8cb73d1d5dc3e207735046401491 Mon Sep 17 00:00:00 2001 From: David Luzar <5153846+dwelle@users.noreply.github.com> Date: Sun, 1 Feb 2026 10:45:04 +0100 Subject: [PATCH 14/32] fix(editor): crop editor cursor drift (#10727) * fix(editor): do not scale cropping editor pointer offsets * fix lint * fix more lint * fix drift related to image canvas scale --- packages/excalidraw/components/App.tsx | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index bbd748d8a8..fa9a02b492 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -11,7 +11,6 @@ import { pointDistance, vector, pointRotateRads, - vectorScale, vectorFromPoint, vectorSubtract, vectorDot, @@ -255,6 +254,7 @@ import { handleFocusPointPointerDown, handleFocusPointPointerUp, maybeHandleArrowPointlikeDrag, + getUncroppedWidthAndHeight, } from "@excalidraw/element"; import type { GlobalPoint, LocalPoint, Radians } from "@excalidraw/math"; @@ -9341,14 +9341,21 @@ class App extends React.Component { this.imageCache.get(croppingElement.fileId)?.image; if (image && !(image instanceof Promise)) { - const instantDragOffset = vectorScale( - vector( - pointerCoords.x - lastPointerCoords.x, - pointerCoords.y - lastPointerCoords.y, - ), - Math.max(this.state.zoom.value, 2), + const uncroppedSize = + getUncroppedWidthAndHeight(croppingElement); + const instantDragOffset = vector( + pointerCoords.x - lastPointerCoords.x, + pointerCoords.y - lastPointerCoords.y, ); + // to reduce cursor:image drift, we need to take into account + // the canvas image element scaling so we can accurately + // track the pixels on movement + instantDragOffset[0] *= + image.naturalWidth / uncroppedSize.width; + instantDragOffset[1] *= + image.naturalHeight / uncroppedSize.height; + const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords( croppingElement, elementsMap, From 54a98268177c19cee7a49d05ce56e64f7a424db1 Mon Sep 17 00:00:00 2001 From: David Luzar <5153846+dwelle@users.noreply.github.com> Date: Sun, 1 Feb 2026 11:06:37 +0100 Subject: [PATCH 15/32] fix(editor): copying to clipboard with no ClipboardEvent (#10729) * fix(editor): copying to clipboard with no ClipboardEvent * fix(editor): use green for `success` state of `FilledButton` --- packages/excalidraw/clipboard.ts | 8 +++----- packages/excalidraw/components/FilledButton.scss | 1 + 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/excalidraw/clipboard.ts b/packages/excalidraw/clipboard.ts index 55fa2b92c3..6033b857af 100644 --- a/packages/excalidraw/clipboard.ts +++ b/packages/excalidraw/clipboard.ts @@ -635,13 +635,13 @@ export const copyTextToSystemClipboard = async < throw new Error("Failed to setData on clipboardEvent"); } } + return; } - return; } catch (error: any) { console.error(error); } - let plainTextEntry = entries.find( + const plainTextEntry = entries.find( ([mimeType]) => mimeType === MIME_TYPES.text, ); @@ -653,9 +653,7 @@ export const copyTextToSystemClipboard = async < // NOTE: doesn't work on FF on non-HTTPS domains, or when document // not focused await navigator.clipboard.writeText(plainTextEntry[1]); - - // invalidate it so we don't write it again below - plainTextEntry = undefined; + return; } catch (error: any) { console.error(error); } diff --git a/packages/excalidraw/components/FilledButton.scss b/packages/excalidraw/components/FilledButton.scss index 431a46a63b..3257a81217 100644 --- a/packages/excalidraw/components/FilledButton.scss +++ b/packages/excalidraw/components/FilledButton.scss @@ -53,6 +53,7 @@ &.ExcButton--status-loading, &.ExcButton--status-success { pointer-events: none; + background-color: var(--color-success); .ExcButton__contents { visibility: hidden; From f39ac4a653335efaaaf9834bf28e9ffc1452cb59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rk=20Tolm=C3=A1cs?= Date: Mon, 2 Feb 2026 20:12:56 +0100 Subject: [PATCH 16/32] fix(editor): On focus drag only update other binding if it's orbit (#10730) fix(focus): Only update other binding if it's orbit Signed-off-by: Mark Tolmacs --- packages/element/src/arrows/focus.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/element/src/arrows/focus.ts b/packages/element/src/arrows/focus.ts index 960524465c..ae02793f72 100644 --- a/packages/element/src/arrows/focus.ts +++ b/packages/element/src/arrows/focus.ts @@ -137,7 +137,7 @@ const focusPointUpdate = ( } // Also update the adjacent end if it has a binding - if (adjacentBinding) { + if (adjacentBinding && adjacentBinding.mode === "orbit") { const adjacentBindableElement = elementsMap.get( adjacentBinding.elementId, ) as ExcalidrawBindableElement; From 83d3943cd0973aa2fe6dc843a730bcb50e8a26ec Mon Sep 17 00:00:00 2001 From: David Luzar <5153846+dwelle@users.noreply.github.com> Date: Thu, 5 Feb 2026 12:00:56 +0100 Subject: [PATCH 17/32] feat(editor): reduce binding gap (#10739) * feat(editor): reduce binding gap to 7px * feat(editor): reduce binding gap to 5px * feat(editor): reduce binding gap to 3px * go back to 5px * update tests --- packages/element/src/binding.ts | 2 +- .../tests/linearElementEditor.test.tsx | 4 +- packages/element/tests/resize.test.tsx | 4 +- .../tests/__snapshots__/history.test.tsx.snap | 278 +++++++++--------- .../tests/__snapshots__/move.test.tsx.snap | 20 +- packages/excalidraw/tests/history.test.tsx | 4 +- packages/excalidraw/tests/move.test.tsx | 6 +- packages/excalidraw/tests/rotate.test.tsx | 4 +- 8 files changed, 161 insertions(+), 161 deletions(-) diff --git a/packages/element/src/binding.ts b/packages/element/src/binding.ts index 074fe2a21f..c763b6e21f 100644 --- a/packages/element/src/binding.ts +++ b/packages/element/src/binding.ts @@ -114,7 +114,7 @@ export type BindingStrategy = * * IMPORTANT: currently must be > 0 (this also applies to the computed gap) */ -export const BASE_BINDING_GAP = 10; +export const BASE_BINDING_GAP = 5; export const BASE_BINDING_GAP_ELBOW = 5; export const FOCUS_POINT_SIZE = 10 / 1.5; diff --git a/packages/element/tests/linearElementEditor.test.tsx b/packages/element/tests/linearElementEditor.test.tsx index 4c9ab3825d..9485dcb222 100644 --- a/packages/element/tests/linearElementEditor.test.tsx +++ b/packages/element/tests/linearElementEditor.test.tsx @@ -1317,7 +1317,7 @@ describe("Test Linear Elements", () => { const textElement = h.elements[2] as ExcalidrawTextElementWithContainer; expect(arrow.endBinding?.elementId).toBe(rect.id); - expect(arrow.width).toBeCloseTo(399); + expect(arrow.width).toBeCloseTo(404); expect(rect.x).toBe(400); expect(rect.y).toBe(0); expect( @@ -1336,7 +1336,7 @@ describe("Test Linear Elements", () => { mouse.downAt(rect.x, rect.y); mouse.moveTo(200, 0); mouse.upAt(200, 0); - expect(arrow.width).toBeCloseTo(199); + expect(arrow.width).toBeCloseTo(204); expect(rect.x).toBe(200); expect(rect.y).toBe(0); expect(handleBindTextResizeSpy).toHaveBeenCalledWith( diff --git a/packages/element/tests/resize.test.tsx b/packages/element/tests/resize.test.tsx index b51d537e37..470cc1fa96 100644 --- a/packages/element/tests/resize.test.tsx +++ b/packages/element/tests/resize.test.tsx @@ -1350,8 +1350,8 @@ describe("multiple selection", () => { expect(boundArrow.x).toBeCloseTo(380 * scaleX); expect(boundArrow.y).toBeCloseTo(240 * scaleY); - expect(boundArrow.points[1][0]).toBeCloseTo(59.7979); - expect(boundArrow.points[1][1]).toBeCloseTo(-79.7305); + expect(boundArrow.points[1][0]).toBeCloseTo(63.4035); + expect(boundArrow.points[1][1]).toBeCloseTo(-84.538); expect(arrowLabelPos.x + arrowLabel.width / 2).toBeCloseTo( boundArrow.x + boundArrow.points[1][0] / 2, diff --git a/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap index 4e9d7bb568..6b8851c281 100644 --- a/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap @@ -198,7 +198,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": "106.79573", + "height": "112.79549", "id": "id4", "index": "a2", "isDeleted": false, @@ -212,8 +212,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl 0, ], [ - "89.00000", - "106.79573", + "94.00000", + "112.79549", ], ], "roughness": 1, @@ -227,8 +227,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 33, - "width": "89.00000", + "version": 34, + "width": "94.00000", "x": 0, "y": 0, } @@ -334,15 +334,15 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl ], "mode": "orbit", }, - "height": "65.91078", + "height": "74.35962", "points": [ [ 0, 0, ], [ - 78, - "65.91078", + 88, + "74.35962", ], ], "startBinding": { @@ -353,27 +353,27 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl ], "mode": "orbit", }, - "version": 32, - "width": 78, + "version": 33, + "width": 88, }, "inserted": { "endBinding": { "elementId": "id1", "fixedPoint": [ - "0.39512", - "0.60488", + "0.39746", + "0.60254", ], "mode": "orbit", }, - "height": "1.30876", + "height": "1.66245", "points": [ [ 0, 0, ], [ - 78, - "-1.30876", + 88, + "-1.66245", ], ], "startBinding": { @@ -384,8 +384,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl ], "mode": "orbit", }, - "version": 29, - "width": 78, + "version": 30, + "width": 88, }, }, }, @@ -428,33 +428,33 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl }, "id4": { "deleted": { - "height": "106.79573", + "height": "112.79549", "points": [ [ 0, 0, ], [ - "89.00000", - "106.79573", + "94.00000", + "112.79549", ], ], "startBinding": null, - "version": 33, - "width": "89.00000", + "version": 34, + "width": "94.00000", "x": 0, "y": 0, }, "inserted": { - "height": "65.91078", + "height": "74.35962", "points": [ [ 0, 0, ], [ - 78, - "65.91078", + 88, + "74.35962", ], ], "startBinding": { @@ -465,10 +465,10 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl ], "mode": "orbit", }, - "version": 32, - "width": 78, - "x": 11, - "y": "49.49137", + "version": 33, + "width": 88, + "x": 6, + "y": "46.67801", }, }, }, @@ -854,7 +854,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 28, + "version": 26, "width": 100, "x": 150, "y": 0, @@ -904,15 +904,15 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "id4": { "deleted": { "endBinding": null, - "height": "4.68000", + "height": "5.28000", "points": [ [ 0, 0, ], [ - -39, - "-4.68000", + -44, + "-5.28000", ], ], "startBinding": { @@ -923,28 +923,28 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl ], "mode": "orbit", }, - "version": 27, - "width": 39, - "y": "4.68000", + "version": 25, + "width": 44, + "y": "5.28000", }, "inserted": { "endBinding": { "elementId": "id1", "fixedPoint": [ - "0.41019", - "0.58981", + "0.41067", + "0.58933", ], "mode": "orbit", }, - "height": "10.70742", + "height": "9.80848", "points": [ [ 0, 0, ], [ - "52.01909", - "10.70742", + "47.06697", + "9.80848", ], ], "startBinding": { @@ -955,9 +955,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl ], "mode": "orbit", }, - "version": 26, - "width": "52.01909", - "y": "-1.72651", + "version": 24, + "width": "47.06697", + "y": "-0.87545", }, }, }, @@ -1004,21 +1004,21 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl ], ], "startBinding": null, - "version": 28, + "version": 26, "width": 100, "x": 150, "y": 0, }, "inserted": { - "height": "4.68000", + "height": "5.28000", "points": [ [ 0, 0, ], [ - -39, - "-4.68000", + -44, + "-5.28000", ], ], "startBinding": { @@ -1029,10 +1029,10 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl ], "mode": "orbit", }, - "version": 27, - "width": 39, - "x": 139, - "y": "4.68000", + "version": 25, + "width": 44, + "x": 144, + "y": "5.28000", }, }, }, @@ -1335,7 +1335,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": "26.16768", + "height": "29.36414", "id": "id4", "index": "Zz", "isDeleted": false, @@ -1349,8 +1349,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl 0, ], [ - "78.00000", - "26.16768", + 88, + "29.36414", ], ], "roughness": 1, @@ -1370,9 +1370,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "type": "arrow", "updated": 1, "version": 10, - "width": "78.00000", - "x": 11, - "y": "3.67566", + "width": 88, + "x": 6, + "y": "2.00946", } `; @@ -1698,7 +1698,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": "10.76674", + "height": "14.91372", "id": "id5", "index": "a0", "isDeleted": false, @@ -1712,8 +1712,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl 0, ], [ - "78.00000", - "-10.76674", + "88.00000", + "-14.91372", ], ], "roughness": 1, @@ -1733,9 +1733,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "type": "arrow", "updated": 1, "version": 11, - "width": "78.00000", - "x": 11, - "y": "35.29320", + "width": "88.00000", + "x": 6, + "y": "37.05219", } `; @@ -1846,7 +1846,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": "10.76674", + "height": "14.91372", "index": "a0", "isDeleted": false, "link": null, @@ -1858,8 +1858,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl 0, ], [ - "78.00000", - "-10.76674", + "88.00000", + "-14.91372", ], ], "roughness": 1, @@ -1878,9 +1878,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "strokeWidth": 2, "type": "arrow", "version": 11, - "width": "78.00000", - "x": 11, - "y": "35.29320", + "width": "88.00000", + "x": 6, + "y": "37.05219", }, "inserted": { "isDeleted": true, @@ -2398,7 +2398,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": "390.11932", + "height": "398.76619", "id": "id4", "index": "a2", "isDeleted": false, @@ -2412,8 +2412,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl 0, ], [ - "478.03878", - "-390.11932", + 488, + "-398.76619", ], ], "roughness": 1, @@ -2435,9 +2435,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "type": "arrow", "updated": 1, "version": 12, - "width": "478.03878", - "x": 11, - "y": "-8.96692", + "width": 488, + "x": 6, + "y": "-4.89286", } `; @@ -2566,7 +2566,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": "390.11932", + "height": "398.76619", "index": "a2", "isDeleted": false, "link": null, @@ -2578,8 +2578,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl 0, ], [ - "478.03878", - "-390.11932", + 488, + "-398.76619", ], ], "roughness": 1, @@ -2600,9 +2600,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "strokeWidth": 2, "type": "arrow", "version": 12, - "width": "478.03878", - "x": 11, - "y": "-8.96692", + "width": 488, + "x": 6, + "y": "-4.89286", }, "inserted": { "isDeleted": true, @@ -16383,7 +16383,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": "0.00108", + "height": "0.00092", "id": "id13", "index": "a3", "isDeleted": false, @@ -16397,8 +16397,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding 0, ], [ - "78.00000", - "0.00108", + "88.00000", + "0.00092", ], ], "roughness": 1, @@ -16420,9 +16420,9 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "type": "arrow", "updated": 1, "version": 12, - "width": "78.00000", - "x": 11, - "y": "0.00807", + "width": "88.00000", + "x": 6, + "y": "0.00849", } `; @@ -16801,7 +16801,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": "0.00561", + "height": "0.00611", "index": "a3", "isDeleted": false, "link": null, @@ -16813,8 +16813,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding 0, ], [ - "78.00000", - "0.00561", + 88, + "0.00611", ], ], "roughness": 1, @@ -16835,8 +16835,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "strokeWidth": 2, "type": "arrow", "version": 8, - "width": "78.00000", - "x": 11, + "width": 88, + "x": 6, "y": 0, }, "inserted": { @@ -17134,7 +17134,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": "0.00108", + "height": "0.00092", "id": "id13", "index": "a3", "isDeleted": false, @@ -17148,8 +17148,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding 0, ], [ - "78.00000", - "0.00108", + "88.00000", + "0.00092", ], ], "roughness": 1, @@ -17171,9 +17171,9 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "type": "arrow", "updated": 1, "version": 12, - "width": "78.00000", - "x": 11, - "y": "0.00807", + "width": "88.00000", + "x": 6, + "y": "0.00849", } `; @@ -17442,7 +17442,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": "0.00108", + "height": "0.00092", "index": "a3", "isDeleted": false, "link": null, @@ -17454,8 +17454,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding 0, ], [ - "78.00000", - "0.00108", + "88.00000", + "0.00092", ], ], "roughness": 1, @@ -17476,9 +17476,9 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "strokeWidth": 2, "type": "arrow", "version": 12, - "width": "78.00000", - "x": 11, - "y": "0.00807", + "width": "88.00000", + "x": 6, + "y": "0.00849", }, "inserted": { "isDeleted": true, @@ -17783,7 +17783,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": "0.00108", + "height": "0.00092", "id": "id13", "index": "a3", "isDeleted": false, @@ -17797,8 +17797,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding 0, ], [ - "78.00000", - "0.00108", + "88.00000", + "0.00092", ], ], "roughness": 1, @@ -17820,9 +17820,9 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "type": "arrow", "updated": 1, "version": 12, - "width": "78.00000", - "x": 11, - "y": "0.00807", + "width": "88.00000", + "x": 6, + "y": "0.00849", } `; @@ -18091,7 +18091,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": "0.00108", + "height": "0.00092", "index": "a3", "isDeleted": false, "link": null, @@ -18103,8 +18103,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding 0, ], [ - "78.00000", - "0.00108", + "88.00000", + "0.00092", ], ], "roughness": 1, @@ -18125,9 +18125,9 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "strokeWidth": 2, "type": "arrow", "version": 12, - "width": "78.00000", - "x": 11, - "y": "0.00807", + "width": "88.00000", + "x": 6, + "y": "0.00849", }, "inserted": { "isDeleted": true, @@ -18430,7 +18430,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": "0.00108", + "height": "0.00092", "id": "id13", "index": "a3", "isDeleted": false, @@ -18444,8 +18444,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding 0, ], [ - "78.00000", - "0.00108", + "88.00000", + "0.00092", ], ], "roughness": 1, @@ -18467,9 +18467,9 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "type": "arrow", "updated": 1, "version": 12, - "width": "78.00000", - "x": 11, - "y": "0.00807", + "width": "88.00000", + "x": 6, + "y": "0.00849", } `; @@ -18824,7 +18824,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": "0.00561", + "height": "0.00611", "index": "a3", "isDeleted": false, "link": null, @@ -18836,8 +18836,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding 0, ], [ - "78.00000", - "0.00561", + 88, + "0.00611", ], ], "roughness": 1, @@ -18858,8 +18858,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "strokeWidth": 2, "type": "arrow", "version": 8, - "width": "78.00000", - "x": 11, + "width": 88, + "x": 6, "y": 0, }, "inserted": { @@ -19185,7 +19185,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": "0.00108", + "height": "0.00092", "id": "id13", "index": "a3", "isDeleted": false, @@ -19199,8 +19199,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding 0, ], [ - "78.00000", - "0.00108", + "88.00000", + "0.00092", ], ], "roughness": 1, @@ -19222,9 +19222,9 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "type": "arrow", "updated": 1, "version": 13, - "width": "78.00000", - "x": 11, - "y": "0.00807", + "width": "88.00000", + "x": 6, + "y": "0.00849", } `; @@ -19575,7 +19575,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": "0.00561", + "height": "0.00611", "index": "a3", "isDeleted": false, "link": null, @@ -19587,8 +19587,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding 0, ], [ - "78.00000", - "0.00561", + 88, + "0.00611", ], ], "roughness": 1, @@ -19609,8 +19609,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "strokeWidth": 2, "type": "arrow", "version": 8, - "width": "78.00000", - "x": 11, + "width": 88, + "x": 6, "y": 0, }, "inserted": { diff --git a/packages/excalidraw/tests/__snapshots__/move.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/move.test.tsx.snap index 6678adef33..ed4531a9e7 100644 --- a/packages/excalidraw/tests/__snapshots__/move.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/move.test.tsx.snap @@ -181,15 +181,15 @@ exports[`move element > rectangles with binding arrow 7`] = ` "endBinding": { "elementId": "id3", "fixedPoint": [ - "-0.03667", - "0.43000", + "-0.02000", + "0.44667", ], "mode": "orbit", }, "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": "80.00000", + "height": "90.00000", "id": "id6", "index": "a2", "isDeleted": false, @@ -203,8 +203,8 @@ exports[`move element > rectangles with binding arrow 7`] = ` 0, ], [ - "79.00000", - "80.00000", + 89, + "90.00000", ], ], "roughness": 1, @@ -216,8 +216,8 @@ exports[`move element > rectangles with binding arrow 7`] = ` "startBinding": { "elementId": "id0", "fixedPoint": [ - "1.11000", - "0.51000", + "1.06000", + "0.46000", ], "mode": "orbit", }, @@ -228,8 +228,8 @@ exports[`move element > rectangles with binding arrow 7`] = ` "updated": 1, "version": 13, "versionNonce": 271613161, - "width": "79.00000", - "x": "111.00000", - "y": "51.00000", + "width": 89, + "x": 106, + "y": "46.00000", } `; diff --git a/packages/excalidraw/tests/history.test.tsx b/packages/excalidraw/tests/history.test.tsx index 014d8608ed..0afaced0cb 100644 --- a/packages/excalidraw/tests/history.test.tsx +++ b/packages/excalidraw/tests/history.test.tsx @@ -4628,7 +4628,7 @@ describe("history", () => { }), endBinding: expect.objectContaining({ elementId: rect2.id, - fixedPoint: [0.41019091151895054, 0.5898090884810495], + fixedPoint: [0.4106696643494564, 0.5893303356505437], mode: "orbit", }), }), @@ -4772,7 +4772,7 @@ describe("history", () => { // rebound with previous rectangle endBinding: expect.objectContaining({ elementId: rect2.id, - fixedPoint: [0.39511653718091, 0.6048834628190899], + fixedPoint: [0.39746300211416496, 0.6025369978858351], mode: "orbit", }), }), diff --git a/packages/excalidraw/tests/move.test.tsx b/packages/excalidraw/tests/move.test.tsx index 659917da8c..05e2951231 100644 --- a/packages/excalidraw/tests/move.test.tsx +++ b/packages/excalidraw/tests/move.test.tsx @@ -110,8 +110,8 @@ describe("move element", () => { expect(h.state.selectedElementIds[rectB.id]).toBeTruthy(); expect([rectA.x, rectA.y]).toEqual([0, 0]); expect([rectB.x, rectB.y]).toEqual([200, 0]); - expect([[arrow.x, arrow.y]]).toCloselyEqualPoints([[111, 51]], 0); - expect([[arrow.width, arrow.height]]).toCloselyEqualPoints([[78, 78]], 0); + expect([[arrow.x, arrow.y]]).toCloselyEqualPoints([[106, 46]], 0); + expect([[arrow.width, arrow.height]]).toCloselyEqualPoints([[88, 88]], 0); renderInteractiveScene.mockClear(); renderStaticScene.mockClear(); @@ -129,7 +129,7 @@ describe("move element", () => { expect(h.state.selectedElementIds[rectB.id]).toBeTruthy(); expect([rectA.x, rectA.y]).toEqual([0, 0]); expect([rectB.x, rectB.y]).toEqual([201, 2]); - expect([[arrow.x, arrow.y]]).toCloselyEqualPoints([[111, 51]], 0); + expect([[arrow.x, arrow.y]]).toCloselyEqualPoints([[106, 46]], 0); expect([[arrow.width, arrow.height]]).toCloselyEqualPoints( [[79, 124.1678]], 2, diff --git a/packages/excalidraw/tests/rotate.test.tsx b/packages/excalidraw/tests/rotate.test.tsx index 47f7e469e4..25597363d6 100644 --- a/packages/excalidraw/tests/rotate.test.tsx +++ b/packages/excalidraw/tests/rotate.test.tsx @@ -80,6 +80,6 @@ test("unselected bound arrows update when rotating their target elements", async expect(textArrow.x).toEqual(360); expect(textArrow.y).toEqual(300); expect(textArrow.points[0]).toEqual([0, 0]); - expect(textArrow.points[1][0]).toBeCloseTo(-95.74, 0); - expect(textArrow.points[1][1]).toBeCloseTo(-119.7354, 0); + expect(textArrow.points[1][0]).toBeCloseTo(-98.87, 0); + expect(textArrow.points[1][1]).toBeCloseTo(-123.65, 0); }); From b43260d97be0cd36faaa630595c299963de2cb6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rk=20Tolm=C3=A1cs?= Date: Fri, 6 Feb 2026 15:01:07 +0100 Subject: [PATCH 18/32] fix: Other binding converted from fixed to orbit unconditionally (#10748) * fix: Other binding converted from fixed to orbit unconditionally Signed-off-by: Mark Tolmacs * fix: New arrow creation Signed-off-by: Mark Tolmacs * fix: Alt point setting on inside binding Signed-off-by: Mark Tolmacs * fix: Initial arrow creation with Alt Signed-off-by: Mark Tolmacs --------- Signed-off-by: Mark Tolmacs --- packages/element/src/binding.ts | 14 +++++++++----- packages/excalidraw/components/App.tsx | 1 + 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/element/src/binding.ts b/packages/element/src/binding.ts index c763b6e21f..afc67d7afd 100644 --- a/packages/element/src/binding.ts +++ b/packages/element/src/binding.ts @@ -810,10 +810,13 @@ const getBindingStrategyForDraggingBindingElementEndpoints_simple = ( elementsMap, ); - const other: BindingStrategy = - otherBindableElement && - !otherFocusPointIsInElement && - appState.selectedLinearElement?.initialState.altFocusPoint + const otherNeverOverride = opts?.newArrow + ? appState.selectedLinearElement?.initialState.arrowStartIsInside + : otherBinding?.mode === "inside"; + const other: BindingStrategy = !otherNeverOverride + ? otherBindableElement && + !otherFocusPointIsInElement && + appState.selectedLinearElement?.initialState.altFocusPoint ? { mode: "orbit", element: otherBindableElement, @@ -832,7 +835,8 @@ const getBindingStrategyForDraggingBindingElementEndpoints_simple = ( elementsMap, ) || otherEndpoint, } - : { mode: undefined }; + : { mode: undefined } + : { mode: undefined }; return { start: startDragged ? current : other, diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index fa9a02b492..88871adde2 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -8743,6 +8743,7 @@ class App extends React.Component { selectedPointsIndices: [endIdx], initialState: { ...linearElementEditor.initialState, + arrowStartIsInside: event.altKey, lastClickedPoint: endIdx, origin: pointFrom( pointerDownState.origin.x, From 063533aede8365bf18b8f08ec949a1e93697c17d Mon Sep 17 00:00:00 2001 From: David Luzar <5153846+dwelle@users.noreply.github.com> Date: Sun, 8 Feb 2026 22:27:34 +0100 Subject: [PATCH 19/32] feat(packages/excalidraw): support nested dropdown menu (#10749) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Barnabás Molnár <38168628+barnabasmolnar@users.noreply.github.com> --- excalidraw-app/components/AppMainMenu.tsx | 2 +- packages/common/src/constants.ts | 1 + packages/excalidraw/components/Actions.tsx | 2 +- .../components/ColorPicker/ColorPicker.tsx | 2 +- .../components/FontPicker/FontPicker.test.tsx | 31 + .../components/FontPicker/FontPicker.tsx | 2 +- .../components/FontPicker/FontPickerList.tsx | 107 ++- .../FontPicker/FontPickerTrigger.tsx | 2 +- packages/excalidraw/components/IconPicker.tsx | 2 +- .../excalidraw/components/LibraryMenu.scss | 3 +- .../excalidraw/components/MobileToolBar.tsx | 3 +- .../components/PropertiesPopover.tsx | 2 +- .../components/Sidebar/SidebarTab.tsx | 2 +- .../components/Sidebar/SidebarTabTrigger.tsx | 2 +- .../components/Sidebar/SidebarTabTriggers.tsx | 2 +- .../components/Sidebar/SidebarTabs.tsx | 2 +- .../components/TTDDialog/Chat/Chat.scss | 2 +- .../TTDDialog/Chat/ChatHistoryMenu.tsx | 6 +- .../components/TTDDialog/TTDDialogTab.tsx | 2 +- .../TTDDialog/TTDDialogTabTrigger.tsx | 2 +- .../TTDDialog/TTDDialogTabTriggers.tsx | 2 +- .../components/TTDDialog/TTDDialogTabs.tsx | 2 +- .../excalidraw/components/ToolPopover.tsx | 2 +- packages/excalidraw/components/UserList.tsx | 2 +- .../components/dropdownMenu/DropdownMenu.scss | 54 +- .../components/dropdownMenu/DropdownMenu.tsx | 43 +- .../dropdownMenu/DropdownMenuContent.tsx | 57 +- .../dropdownMenu/DropdownMenuItem.tsx | 67 +- .../DropdownMenuItemContentRadio.tsx | 4 +- .../dropdownMenu/DropdownMenuItemLink.tsx | 35 +- .../dropdownMenu/DropdownMenuSeparator.tsx | 2 +- .../dropdownMenu/DropdownMenuSub.tsx | 26 + .../dropdownMenu/DropdownMenuSubContent.tsx | 71 ++ .../dropdownMenu/DropdownMenuSubTrigger.tsx | 38 + .../dropdownMenu/DropdownMenuTrigger.tsx | 6 +- .../components/dropdownMenu/common.ts | 25 +- .../dropdownMenu/dropdownMenuUtils.ts | 28 +- packages/excalidraw/components/icons.tsx | 12 + .../components/main-menu/DefaultItems.tsx | 8 +- .../components/main-menu/MainMenu.tsx | 10 +- packages/excalidraw/css/styles.scss | 16 + packages/excalidraw/package.json | 3 +- .../MermaidToExcalidraw.test.tsx.snap | 2 +- .../__snapshots__/excalidraw.test.tsx.snap | 128 ++- .../excalidraw/wysiwyg/textWysiwyg.test.tsx | 1 - yarn.lock | 873 ++++++++++++++---- 46 files changed, 1265 insertions(+), 431 deletions(-) create mode 100644 packages/excalidraw/components/FontPicker/FontPicker.test.tsx create mode 100644 packages/excalidraw/components/dropdownMenu/DropdownMenuSub.tsx create mode 100644 packages/excalidraw/components/dropdownMenu/DropdownMenuSubContent.tsx create mode 100644 packages/excalidraw/components/dropdownMenu/DropdownMenuSubTrigger.tsx diff --git a/excalidraw-app/components/AppMainMenu.tsx b/excalidraw-app/components/AppMainMenu.tsx index cd0aca2683..e51d1763b8 100644 --- a/excalidraw-app/components/AppMainMenu.tsx +++ b/excalidraw-app/components/AppMainMenu.tsx @@ -62,7 +62,7 @@ export const AppMainMenu: React.FC<{ {isDevEnv() && ( { + onSelect={() => { if (window.visualDebug) { delete window.visualDebug; saveDebugState({ enabled: false }); diff --git a/packages/common/src/constants.ts b/packages/common/src/constants.ts index 03978584cb..4ff50335ef 100644 --- a/packages/common/src/constants.ts +++ b/packages/common/src/constants.ts @@ -106,6 +106,7 @@ export const CLASSES = { CONVERT_ELEMENT_TYPE_POPUP: "ConvertElementTypePopup", SHAPE_ACTIONS_THEME_SCOPE: "shape-actions-theme-scope", FRAME_NAME: "frame-name", + DROPDOWN_MENU_EVENT_WRAPPER: "dropdown-menu-event-wrapper", }; export const FONT_SIZES = { diff --git a/packages/excalidraw/components/Actions.tsx b/packages/excalidraw/components/Actions.tsx index 18791f8f9b..d9f3415d64 100644 --- a/packages/excalidraw/components/Actions.tsx +++ b/packages/excalidraw/components/Actions.tsx @@ -1,6 +1,6 @@ import clsx from "clsx"; import { useRef, useState } from "react"; -import * as Popover from "@radix-ui/react-popover"; +import { Popover } from "radix-ui"; import { CLASSES, diff --git a/packages/excalidraw/components/ColorPicker/ColorPicker.tsx b/packages/excalidraw/components/ColorPicker/ColorPicker.tsx index 5de89f7590..f76c109280 100644 --- a/packages/excalidraw/components/ColorPicker/ColorPicker.tsx +++ b/packages/excalidraw/components/ColorPicker/ColorPicker.tsx @@ -1,4 +1,4 @@ -import * as Popover from "@radix-ui/react-popover"; +import { Popover } from "radix-ui"; import clsx from "clsx"; import { useRef, useEffect } from "react"; diff --git a/packages/excalidraw/components/FontPicker/FontPicker.test.tsx b/packages/excalidraw/components/FontPicker/FontPicker.test.tsx new file mode 100644 index 0000000000..ab92464fd5 --- /dev/null +++ b/packages/excalidraw/components/FontPicker/FontPicker.test.tsx @@ -0,0 +1,31 @@ +import { KEYS } from "@excalidraw/common"; + +import { Excalidraw } from "../.."; +import { Keyboard } from "../../tests/helpers/ui"; +import { act, render } from "../../tests/test-utils"; + +describe("FontPicker", () => { + it("should be able to open font picker", async () => { + (global as any).ResizeObserver = + (global as any).ResizeObserver || + class ResizeObserver { + observe() {} + unobserve() {} + disconnect() {} + }; + + const { queryByTestId } = await render( + , + ); + + Keyboard.keyPress(KEYS.T); + + const fontPickerTrigger = queryByTestId("font-family-show-fonts"); + + expect(fontPickerTrigger).not.toBeNull(); + + act(() => { + fontPickerTrigger!.click(); + }); + }); +}); diff --git a/packages/excalidraw/components/FontPicker/FontPicker.tsx b/packages/excalidraw/components/FontPicker/FontPicker.tsx index c52286a173..4325147e17 100644 --- a/packages/excalidraw/components/FontPicker/FontPicker.tsx +++ b/packages/excalidraw/components/FontPicker/FontPicker.tsx @@ -1,4 +1,4 @@ -import * as Popover from "@radix-ui/react-popover"; +import { Popover } from "radix-ui"; import clsx from "clsx"; import React, { useCallback, useMemo } from "react"; diff --git a/packages/excalidraw/components/FontPicker/FontPickerList.tsx b/packages/excalidraw/components/FontPicker/FontPickerList.tsx index 79dca68ce0..e5e7b85aea 100644 --- a/packages/excalidraw/components/FontPicker/FontPickerList.tsx +++ b/packages/excalidraw/components/FontPicker/FontPickerList.tsx @@ -30,10 +30,12 @@ import { PropertiesPopover } from "../PropertiesPopover"; import { QuickSearch } from "../QuickSearch"; import { ScrollableList } from "../ScrollableList"; import DropdownMenuGroup from "../dropdownMenu/DropdownMenuGroup"; -import DropdownMenuItem, { +import { DropDownMenuItemBadgeType, DropDownMenuItemBadge, } from "../dropdownMenu/DropdownMenuItem"; +import MenuItemContent from "../dropdownMenu/DropdownMenuItemContent"; +import { getDropdownMenuItemClassName } from "../dropdownMenu/common"; import { FontFamilyCodeIcon, FontFamilyHeadingIcon, @@ -269,45 +271,74 @@ export const FontPickerList = React.memo( [filteredFonts, sceneFamilies], ); - const renderFont = (font: FontDescriptor, index: number) => ( - { - wrappedOnSelect(Number(e.currentTarget.value)); - }} - onMouseMove={() => { - if (hoveredFont?.value !== font.value) { - onHover(font.value); - } - }} - badge={ - font.badge && ( - - {font.badge.placeholder} - - ) + const FontPickerListItem = ({ + font, + order, + }: { + font: FontDescriptor; + order: number; + }) => { + const ref = useRef(null); + const isHovered = font.value === hoveredFont?.value; + const isSelected = font.value === selectedFontFamily; + + useEffect(() => { + if (!isHovered) { + return; } - > - {font.text} - - ); + if (order === 0) { + // scroll into the first item differently, so it's visible what is above (i.e. group title) + ref.current?.scrollIntoView?.({ block: "end" }); + } else { + ref.current?.scrollIntoView?.({ block: "nearest" }); + } + }, [isHovered, order]); + + return ( + + ); + }; const groups = []; if (sceneFilteredFonts.length) { groups.push( - {sceneFilteredFonts.map(renderFont)} + {sceneFilteredFonts.map((font, index) => ( + + ))} , ); } @@ -315,9 +346,13 @@ export const FontPickerList = React.memo( if (availableFilteredFonts.length) { groups.push( - {availableFilteredFonts.map((font, index) => - renderFont(font, index + sceneFilteredFonts.length), - )} + {availableFilteredFonts.map((font, index) => ( + + ))} , ); } diff --git a/packages/excalidraw/components/FontPicker/FontPickerTrigger.tsx b/packages/excalidraw/components/FontPicker/FontPickerTrigger.tsx index ede3a50e06..eea1cdb8d1 100644 --- a/packages/excalidraw/components/FontPicker/FontPickerTrigger.tsx +++ b/packages/excalidraw/components/FontPicker/FontPickerTrigger.tsx @@ -1,4 +1,4 @@ -import * as Popover from "@radix-ui/react-popover"; +import { Popover } from "radix-ui"; import { MOBILE_ACTION_BUTTON_BG } from "@excalidraw/common"; diff --git a/packages/excalidraw/components/IconPicker.tsx b/packages/excalidraw/components/IconPicker.tsx index fab4f109b8..0d644ca7e9 100644 --- a/packages/excalidraw/components/IconPicker.tsx +++ b/packages/excalidraw/components/IconPicker.tsx @@ -1,4 +1,4 @@ -import * as Popover from "@radix-ui/react-popover"; +import { Popover } from "radix-ui"; import clsx from "clsx"; import React, { useEffect } from "react"; diff --git a/packages/excalidraw/components/LibraryMenu.scss b/packages/excalidraw/components/LibraryMenu.scss index 6133bb2ecf..b0892a07f7 100644 --- a/packages/excalidraw/components/LibraryMenu.scss +++ b/packages/excalidraw/components/LibraryMenu.scss @@ -126,9 +126,10 @@ .dropdown-menu-container { width: 196px; - box-shadow: var(--library-dropdown-shadow); border-radius: var(--border-radius-lg); padding: 0.25rem 0.5rem; + + --box-shadow: var(--library-dropdown-shadow); } } diff --git a/packages/excalidraw/components/MobileToolBar.tsx b/packages/excalidraw/components/MobileToolBar.tsx index 9cd351d2ee..e439061217 100644 --- a/packages/excalidraw/components/MobileToolBar.tsx +++ b/packages/excalidraw/components/MobileToolBar.tsx @@ -375,7 +375,7 @@ export const MobileToolBar = ({ )} {/* Other Shapes */} - + setIsOtherShapesMenuOpen(false)} onSelect={() => setIsOtherShapesMenuOpen(false)} className="App-toolbar__extra-tools-dropdown" + align="start" > {!showTextToolOutside && ( {historyIcon} - + <> {savedChats.map((chat) => ( { const MenuTriggerComp = getMenuTriggerComponent(children); const MenuContentComp = getMenuContentComponent(children); - - // clone the MenuContentComp to pass the placement prop - const MenuContentCompWithPlacement = + const MenuContentWithState = MenuContentComp && React.isValidElement(MenuContentComp) - ? React.cloneElement(MenuContentComp as React.ReactElement, { - placement, - }) + ? React.cloneElement( + MenuContentComp as React.ReactElement< + React.ComponentProps + >, + { open }, + ) : MenuContentComp; return ( -
- {MenuTriggerComp} - {open && MenuContentCompWithPlacement} -
+ +
+ {MenuTriggerComp} + {MenuContentWithState} +
+
); }; @@ -55,6 +61,7 @@ DropdownMenu.ItemLink = DropdownMenuItemLink; DropdownMenu.ItemCustom = DropdownMenuItemCustom; DropdownMenu.Group = DropdownMenuGroup; DropdownMenu.Separator = MenuSeparator; +DropdownMenu.Sub = DropdownMenuSub; export default DropdownMenu; diff --git a/packages/excalidraw/components/dropdownMenu/DropdownMenuContent.tsx b/packages/excalidraw/components/dropdownMenu/DropdownMenuContent.tsx index f92c9df327..79be24b969 100644 --- a/packages/excalidraw/components/dropdownMenu/DropdownMenuContent.tsx +++ b/packages/excalidraw/components/dropdownMenu/DropdownMenuContent.tsx @@ -1,7 +1,9 @@ import clsx from "clsx"; -import React, { useEffect, useRef } from "react"; +import React, { useCallback, useEffect, useRef } from "react"; -import { EVENT, KEYS } from "@excalidraw/common"; +import { CLASSES, EVENT, KEYS } from "@excalidraw/common"; + +import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"; import { useOutsideClick } from "../../hooks/useOutsideClick"; import { useStable } from "../../hooks/useStable"; @@ -16,8 +18,9 @@ const MenuContent = ({ onClickOutside, className = "", onSelect, + open = true, + align = "end", style, - placement = "bottom", }: { children?: React.ReactNode; onClickOutside?: () => void; @@ -26,26 +29,36 @@ const MenuContent = ({ * Called when any menu item is selected (clicked on). */ onSelect?: (event: Event) => void; + open?: boolean; style?: React.CSSProperties; - placement?: "top" | "bottom"; + align?: "start" | "center" | "end"; }) => { const editorInterface = useEditorInterface(); const menuRef = useRef(null); const callbacksRef = useStable({ onClickOutside }); - useOutsideClick(menuRef, (event) => { - // prevents closing if clicking on the trigger button - if ( - !menuRef.current - ?.closest(".dropdown-menu-container") - ?.contains(event.target) - ) { - callbacksRef.onClickOutside?.(); - } - }); + useOutsideClick( + menuRef, + useCallback( + (event) => { + // prevents closing if clicking on the trigger button + if ( + !menuRef.current + ?.closest(`.${CLASSES.DROPDOWN_MENU_EVENT_WRAPPER}`) + ?.contains(event.target) + ) { + callbacksRef.onClickOutside?.(); + } + }, + [callbacksRef], + ), + ); useEffect(() => { + if (!open) { + return; + } const onKeyDown = (event: KeyboardEvent) => { if (event.key === KEYS.ESCAPE) { event.stopImmediatePropagation(); @@ -63,35 +76,33 @@ const MenuContent = ({ return () => { document.removeEventListener(EVENT.KEYDOWN, onKeyDown, option); }; - }, [callbacksRef]); + }, [callbacksRef, open]); const classNames = clsx(`dropdown-menu ${className}`, { "dropdown-menu--mobile": editorInterface.formFactor === "phone", - "dropdown-menu--placement-top": placement === "top", }).trim(); return ( -
event.preventDefault()} > {/* the zIndex ensures this menu has higher stacking order, see https://github.com/excalidraw/excalidraw/pull/1445 */} {editorInterface.formFactor === "phone" ? ( {children} ) : ( - + {children} )} -
+
); }; diff --git a/packages/excalidraw/components/dropdownMenu/DropdownMenuItem.tsx b/packages/excalidraw/components/dropdownMenu/DropdownMenuItem.tsx index 0f227a0bbd..7cc10b95d4 100644 --- a/packages/excalidraw/components/dropdownMenu/DropdownMenuItem.tsx +++ b/packages/excalidraw/components/dropdownMenu/DropdownMenuItem.tsx @@ -1,78 +1,63 @@ -import React, { useEffect, useRef } from "react"; +import React from "react"; import { THEME } from "@excalidraw/common"; +import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"; + import type { ValueOf } from "@excalidraw/common/utility-types"; import { useExcalidrawAppState } from "../App"; -import MenuItemContent from "./DropdownMenuItemContent"; import { getDropdownMenuItemClassName, - useHandleDropdownMenuItemClick, + useHandleDropdownMenuItemSelect, } from "./common"; +import MenuItemContent from "./DropdownMenuItemContent"; import type { JSX } from "react"; const DropdownMenuItem = ({ icon, + badge, value, - order, children, shortcut, className, - hovered, selected, - textStyle, onSelect, - onClick, - badge, ...rest }: { icon?: JSX.Element; + badge?: React.ReactNode; value?: string | number | undefined; - order?: number; onSelect?: (event: Event) => void; children: React.ReactNode; shortcut?: string; - hovered?: boolean; selected?: boolean; - textStyle?: React.CSSProperties; className?: string; - badge?: React.ReactNode; -} & Omit, "onSelect">) => { - const handleClick = useHandleDropdownMenuItemClick(onClick, onSelect); - const ref = useRef(null); - - useEffect(() => { - if (hovered) { - if (order === 0) { - // scroll into the first item differently, so it's visible what is above (i.e. group title) - ref.current?.scrollIntoView({ block: "end" }); - } else { - ref.current?.scrollIntoView({ block: "nearest" }); - } - } - }, [hovered, order]); +} & Omit< + React.ButtonHTMLAttributes, + "onSelect" | "onClick" +>) => { + const handleSelect = useHandleDropdownMenuItemSelect(onSelect); return ( - + + {children} + + + ); }; DropdownMenuItem.displayName = "DropdownMenuItem"; diff --git a/packages/excalidraw/components/dropdownMenu/DropdownMenuItemContentRadio.tsx b/packages/excalidraw/components/dropdownMenu/DropdownMenuItemContentRadio.tsx index d8177c50e0..4f2986c30e 100644 --- a/packages/excalidraw/components/dropdownMenu/DropdownMenuItemContentRadio.tsx +++ b/packages/excalidraw/components/dropdownMenu/DropdownMenuItemContentRadio.tsx @@ -27,9 +27,7 @@ const DropdownMenuItemContentRadio = ({ return ( <>
- + void; rel?: string; } & React.AnchorHTMLAttributes) => { - const handleClick = useHandleDropdownMenuItemClick(rest.onClick, onSelect); + const handleSelect = useHandleDropdownMenuItemSelect(onSelect); return ( // eslint-disable-next-line react/jsx-no-target-blank - - - {children} - - + + + {children} + + + ); }; diff --git a/packages/excalidraw/components/dropdownMenu/DropdownMenuSeparator.tsx b/packages/excalidraw/components/dropdownMenu/DropdownMenuSeparator.tsx index 1c6d19521d..4f880c691a 100644 --- a/packages/excalidraw/components/dropdownMenu/DropdownMenuSeparator.tsx +++ b/packages/excalidraw/components/dropdownMenu/DropdownMenuSeparator.tsx @@ -5,7 +5,7 @@ const MenuSeparator = () => ( style={{ height: "1px", backgroundColor: "var(--default-border-color)", - margin: ".5rem 0", + margin: "6px 0", flex: "0 0 auto", }} /> diff --git a/packages/excalidraw/components/dropdownMenu/DropdownMenuSub.tsx b/packages/excalidraw/components/dropdownMenu/DropdownMenuSub.tsx new file mode 100644 index 0000000000..0ba328b189 --- /dev/null +++ b/packages/excalidraw/components/dropdownMenu/DropdownMenuSub.tsx @@ -0,0 +1,26 @@ +import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"; + +import DropdownMenuSubContent from "./DropdownMenuSubContent"; +import DropdownMenuSubTrigger from "./DropdownMenuSubTrigger"; +import { + getSubMenuContentComponent, + getSubMenuTriggerComponent, +} from "./dropdownMenuUtils"; + +const DropdownMenuSub = ({ children }: { children?: React.ReactNode }) => { + const MenuTriggerComp = getSubMenuTriggerComponent(children); + const MenuContentComp = getSubMenuContentComponent(children); + return ( + + {MenuTriggerComp} + {MenuContentComp} + + ); +}; + +DropdownMenuSub.Trigger = DropdownMenuSubTrigger; +DropdownMenuSub.Content = DropdownMenuSubContent; + +DropdownMenuSub.displayName = "DropdownMenuSub"; + +export default DropdownMenuSub; diff --git a/packages/excalidraw/components/dropdownMenu/DropdownMenuSubContent.tsx b/packages/excalidraw/components/dropdownMenu/DropdownMenuSubContent.tsx new file mode 100644 index 0000000000..ef5debef57 --- /dev/null +++ b/packages/excalidraw/components/dropdownMenu/DropdownMenuSubContent.tsx @@ -0,0 +1,71 @@ +import clsx from "clsx"; + +import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"; + +import { useCallback, useState } from "react"; + +import { useEditorInterface } from "../App"; +import { Island } from "../Island"; +import Stack from "../Stack"; + +const BASE_ALIGN_OFFSET = -4; +const BASE_SIDE_OFFSET = 4; + +const DropdownMenuSubContent = ({ + children, + className, +}: { + children?: React.ReactNode; + className?: string; +}) => { + const editorInterface = useEditorInterface(); + + const classNames = clsx(`dropdown-menu dropdown-submenu ${className}`, { + "dropdown-menu--mobile": editorInterface.formFactor === "phone", + }).trim(); + + const callbacksRef = useCallback((node: HTMLDivElement | null) => { + if (node) { + const parentContainer = node.closest(".dropdown-menu-container"); + const parentRect = parentContainer?.getBoundingClientRect(); + if (parentRect) { + const menuWidth = node.getBoundingClientRect().width; + + const viewportWidth = window.innerWidth; + const spaceRemaining = viewportWidth - parentRect.right; + if (spaceRemaining < menuWidth + 20) { + setSideOffset(spaceRemaining - menuWidth + BASE_ALIGN_OFFSET); + setAlignOffset(BASE_ALIGN_OFFSET + 8); + } + } + } + }, []); + + const [sideOffset, setSideOffset] = useState(BASE_SIDE_OFFSET); + const [alignOffset, setAlignOffset] = useState(BASE_ALIGN_OFFSET); + + return ( + + {editorInterface.formFactor === "phone" ? ( + {children} + ) : ( + + {children} + + )} + + ); +}; + +export default DropdownMenuSubContent; +DropdownMenuSubContent.displayName = "DropdownMenuSubContent"; diff --git a/packages/excalidraw/components/dropdownMenu/DropdownMenuSubTrigger.tsx b/packages/excalidraw/components/dropdownMenu/DropdownMenuSubTrigger.tsx new file mode 100644 index 0000000000..579d4979a7 --- /dev/null +++ b/packages/excalidraw/components/dropdownMenu/DropdownMenuSubTrigger.tsx @@ -0,0 +1,38 @@ +import React from "react"; + +import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"; + +import { chevronRight } from "../icons"; + +import { getDropdownMenuItemClassName } from "./common"; +import MenuItemContent from "./DropdownMenuItemContent"; + +import type { JSX } from "react"; + +const DropdownMenuSubTrigger = ({ + children, + icon, + shortcut, + className, +}: { + children: React.ReactNode; + icon?: JSX.Element; + shortcut?: string; + className?: string; +}) => { + return ( + + + {children} + +
{chevronRight}
+
+ ); +}; + +export default DropdownMenuSubTrigger; +DropdownMenuSubTrigger.displayName = "DropdownMenuSubTrigger"; diff --git a/packages/excalidraw/components/dropdownMenu/DropdownMenuTrigger.tsx b/packages/excalidraw/components/dropdownMenu/DropdownMenuTrigger.tsx index e1f3ef202f..469eb002b8 100644 --- a/packages/excalidraw/components/dropdownMenu/DropdownMenuTrigger.tsx +++ b/packages/excalidraw/components/dropdownMenu/DropdownMenuTrigger.tsx @@ -1,5 +1,7 @@ import clsx from "clsx"; +import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"; + import { useEditorInterface } from "../App"; const MenuTrigger = ({ @@ -23,7 +25,7 @@ const MenuTrigger = ({ }, ).trim(); return ( - + ); }; diff --git a/packages/excalidraw/components/dropdownMenu/common.ts b/packages/excalidraw/components/dropdownMenu/common.ts index feca5c4060..27342a28f6 100644 --- a/packages/excalidraw/components/dropdownMenu/common.ts +++ b/packages/excalidraw/components/dropdownMenu/common.ts @@ -1,6 +1,6 @@ import React, { useContext } from "react"; -import { EVENT, composeEventHandlers } from "@excalidraw/common"; +import { composeEventHandlers } from "@excalidraw/common"; export const DropdownMenuContentPropsContext = React.createContext<{ onSelect?: (event: Event) => void; @@ -11,28 +11,17 @@ export const getDropdownMenuItemClassName = ( selected = false, hovered = false, ) => { - return `dropdown-menu-item dropdown-menu-item-base ${className} - ${selected ? "dropdown-menu-item--selected" : ""} ${ - hovered ? "dropdown-menu-item--hovered" : "" - }`.trim(); + return `dropdown-menu-item dropdown-menu-item-base ${className} ${ + selected ? "dropdown-menu-item--selected" : "" + } ${hovered ? "dropdown-menu-item--hovered" : ""}`.trim(); }; -export const useHandleDropdownMenuItemClick = ( - origOnClick: - | React.MouseEventHandler - | undefined, +export const useHandleDropdownMenuItemSelect = ( onSelect: ((event: Event) => void) | undefined, ) => { const DropdownMenuContentProps = useContext(DropdownMenuContentPropsContext); - return composeEventHandlers(origOnClick, (event) => { - const itemSelectEvent = new CustomEvent(EVENT.MENU_ITEM_SELECT, { - bubbles: true, - cancelable: true, - }); - onSelect?.(itemSelectEvent); - if (!itemSelectEvent.defaultPrevented) { - DropdownMenuContentProps.onSelect?.(itemSelectEvent); - } + return composeEventHandlers(onSelect, (event) => { + DropdownMenuContentProps.onSelect?.(event); }); }; diff --git a/packages/excalidraw/components/dropdownMenu/dropdownMenuUtils.ts b/packages/excalidraw/components/dropdownMenu/dropdownMenuUtils.ts index 10d91fb856..82e8ccf591 100644 --- a/packages/excalidraw/components/dropdownMenu/dropdownMenuUtils.ts +++ b/packages/excalidraw/components/dropdownMenu/dropdownMenuUtils.ts @@ -1,6 +1,6 @@ import React from "react"; -export const getMenuTriggerComponent = (children: React.ReactNode) => { +const getMenuComponent = (component: string) => (children: React.ReactNode) => { const comp = React.Children.toArray(children).find( (child) => React.isValidElement(child) && @@ -8,7 +8,7 @@ export const getMenuTriggerComponent = (children: React.ReactNode) => { //@ts-ignore child?.type.displayName && //@ts-ignore - child.type.displayName === "DropdownMenuTrigger", + child.type.displayName === component, ); if (!comp) { return null; @@ -17,19 +17,11 @@ export const getMenuTriggerComponent = (children: React.ReactNode) => { return comp; }; -export const getMenuContentComponent = (children: React.ReactNode) => { - const comp = React.Children.toArray(children).find( - (child) => - React.isValidElement(child) && - typeof child.type !== "string" && - //@ts-ignore - child?.type.displayName && - //@ts-ignore - child.type.displayName === "DropdownMenuContent", - ); - if (!comp) { - return null; - } - //@ts-ignore - return comp; -}; +export const getMenuTriggerComponent = getMenuComponent("DropdownMenuTrigger"); +export const getMenuContentComponent = getMenuComponent("DropdownMenuContent"); +export const getSubMenuTriggerComponent = getMenuComponent( + "DropdownMenuSubTrigger", +); +export const getSubMenuContentComponent = getMenuComponent( + "DropdownMenuSubContent", +); diff --git a/packages/excalidraw/components/icons.tsx b/packages/excalidraw/components/icons.tsx index 8caf6cc163..f5ce947d7a 100644 --- a/packages/excalidraw/components/icons.tsx +++ b/packages/excalidraw/components/icons.tsx @@ -2396,3 +2396,15 @@ export const presentationIcon = createIcon( , tablerIconProps, ); + +// empty placeholder icon (used for alignment in menus) +export const emptyIcon =
; + +//tabler-icons: chevron-right +export const chevronRight = createIcon( + + + + , + tablerIconProps, +); diff --git a/packages/excalidraw/components/main-menu/DefaultItems.tsx b/packages/excalidraw/components/main-menu/DefaultItems.tsx index 29a2761a10..2a00a79a71 100644 --- a/packages/excalidraw/components/main-menu/DefaultItems.tsx +++ b/packages/excalidraw/components/main-menu/DefaultItems.tsx @@ -306,10 +306,14 @@ export const ChangeCanvasBackground = () => { return null; } return ( -
+
{t("labels.canvasBackground")}
diff --git a/packages/excalidraw/components/main-menu/MainMenu.tsx b/packages/excalidraw/components/main-menu/MainMenu.tsx index d028231328..3755f1f0d4 100644 --- a/packages/excalidraw/components/main-menu/MainMenu.tsx +++ b/packages/excalidraw/components/main-menu/MainMenu.tsx @@ -8,6 +8,7 @@ import { t } from "../../i18n"; import { useEditorInterface, useExcalidrawSetAppState } from "../App"; import { UserList } from "../UserList"; import DropdownMenu from "../dropdownMenu/DropdownMenu"; +import DropdownMenuSub from "../dropdownMenu/DropdownMenuSub"; import { withInternalFallback } from "../hoc/withInternalFallback"; import { HamburgerMenuIcon } from "../icons"; @@ -52,12 +53,8 @@ const MainMenu = Object.assign( onSelect={composeEventHandlers(onSelect, () => { setAppState({ openMenu: null }); })} - placement="bottom" - className={ - editorInterface.formFactor === "phone" - ? "main-menu-dropdown" - : "" - } + className="main-menu" + align="start" > {children} {editorInterface.formFactor === "phone" && @@ -84,6 +81,7 @@ const MainMenu = Object.assign( ItemCustom: DropdownMenu.ItemCustom, Group: DropdownMenu.Group, Separator: DropdownMenu.Separator, + Sub: DropdownMenuSub, DefaultItems, }, ); diff --git a/packages/excalidraw/css/styles.scss b/packages/excalidraw/css/styles.scss index 5557d50313..e0d42fa716 100644 --- a/packages/excalidraw/css/styles.scss +++ b/packages/excalidraw/css/styles.scss @@ -16,6 +16,7 @@ --zIndex-ui-context-menu: 90; --zIndex-ui-styles-popup: 100; --zIndex-ui-top: 100; + --zIndex-ui-main-menu: 110; --zIndex-ui-library: 120; --zIndex-modal: 1000; @@ -223,6 +224,18 @@ body.excalidraw-cursor-resize * { box-shadow: 0 0 0 1px var(--color-brand-hover); } + // radix doesn't allow differntiating between hover and keyboard active + // states (it's forcing :focus on both). + // + // proper handling would be to disable :focus-visible by default, and enable + // on keyboard arrows (it'd then have to be disabled again, e.g. on keydown + // or container focus) + // + // alas, that is left for another day + [data-radix-collection-item]:focus-visible { + box-shadow: none !important; + } + .buttonList { .ToolIcon__icon { all: unset !important; @@ -670,6 +683,9 @@ body.excalidraw-cursor-resize * { } } + .main-menu { + z-index: var(--zIndex-ui-main-menu); + } .main-menu-trigger { @include filledButtonOnCanvas; } diff --git a/packages/excalidraw/package.json b/packages/excalidraw/package.json index c7d8693d86..2f12bf6041 100644 --- a/packages/excalidraw/package.json +++ b/packages/excalidraw/package.json @@ -85,8 +85,7 @@ "@excalidraw/math": "0.18.0", "@excalidraw/mermaid-to-excalidraw": "2.0.0-rfc3", "@excalidraw/random-username": "1.1.0", - "@radix-ui/react-popover": "1.1.6", - "@radix-ui/react-tabs": "1.1.3", + "radix-ui": "1.4.3", "browser-fs-access": "0.29.1", "canvas-roundrect-polyfill": "0.0.1", "clsx": "1.1.1", diff --git a/packages/excalidraw/tests/__snapshots__/MermaidToExcalidraw.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/MermaidToExcalidraw.test.tsx.snap index 1d92ee37e0..f07f054977 100644 --- a/packages/excalidraw/tests/__snapshots__/MermaidToExcalidraw.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/MermaidToExcalidraw.test.tsx.snap @@ -1,7 +1,7 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`Test > should open mermaid popup when active tool is mermaid 1`] = ` -"