mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-04-14 16:40:58 -04:00
Merge remote-tracking branch 'origin/master' into aakansha-create-text-containers-programmatically
This commit is contained in:
commit
d9e6910df0
129 changed files with 5479 additions and 2658 deletions
|
@ -34,7 +34,7 @@
|
|||
"i18next-browser-languagedetector": "6.1.4",
|
||||
"idb-keyval": "6.0.3",
|
||||
"image-blob-reduce": "3.0.1",
|
||||
"jotai": "1.6.4",
|
||||
"jotai": "1.13.1",
|
||||
"lodash.throttle": "4.1.1",
|
||||
"nanoid": "3.3.3",
|
||||
"open-color": "1.9.1",
|
||||
|
|
|
@ -26,7 +26,7 @@ export const actionChangeProjectName = register({
|
|||
perform: (_elements, appState, value) => {
|
||||
return { appState: { ...appState, name: value }, commitToHistory: false };
|
||||
},
|
||||
PanelComponent: ({ appState, updateData, appProps }) => (
|
||||
PanelComponent: ({ appState, updateData, appProps, data }) => (
|
||||
<ProjectName
|
||||
label={t("labels.fileTitle")}
|
||||
value={appState.name || "Unnamed"}
|
||||
|
@ -34,6 +34,7 @@ export const actionChangeProjectName = register({
|
|||
isNameEditable={
|
||||
typeof appProps.name === "undefined" && !appState.viewModeEnabled
|
||||
}
|
||||
ignoreFocus={data?.ignoreFocus ?? false}
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
|
|
@ -1,42 +1,17 @@
|
|||
import { register } from "./register";
|
||||
import { getSelectedElements } from "../scene";
|
||||
import { getNonDeletedElements } from "../element";
|
||||
import { mutateElement } from "../element/mutateElement";
|
||||
import { ExcalidrawElement, NonDeleted } from "../element/types";
|
||||
import { normalizeAngle, resizeSingleElement } from "../element/resizeElements";
|
||||
import { AppState } from "../types";
|
||||
import { getTransformHandles } from "../element/transformHandles";
|
||||
import { updateBoundElements } from "../element/binding";
|
||||
import { resizeMultipleElements } from "../element/resizeElements";
|
||||
import { AppState, PointerDownState } from "../types";
|
||||
import { arrayToMap } from "../utils";
|
||||
import {
|
||||
getElementAbsoluteCoords,
|
||||
getElementPointsCoords,
|
||||
} from "../element/bounds";
|
||||
import { isLinearElement } from "../element/typeChecks";
|
||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||
import { CODES, KEYS } from "../keys";
|
||||
|
||||
const enableActionFlipHorizontal = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
appState: AppState,
|
||||
) => {
|
||||
const eligibleElements = getSelectedElements(
|
||||
getNonDeletedElements(elements),
|
||||
appState,
|
||||
);
|
||||
return eligibleElements.length === 1 && eligibleElements[0].type !== "text";
|
||||
};
|
||||
|
||||
const enableActionFlipVertical = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
appState: AppState,
|
||||
) => {
|
||||
const eligibleElements = getSelectedElements(
|
||||
getNonDeletedElements(elements),
|
||||
appState,
|
||||
);
|
||||
return eligibleElements.length === 1;
|
||||
};
|
||||
import { getCommonBoundingBox } from "../element/bounds";
|
||||
import {
|
||||
bindOrUnbindSelectedElements,
|
||||
isBindingEnabled,
|
||||
unbindLinearElements,
|
||||
} from "../element/binding";
|
||||
|
||||
export const actionFlipHorizontal = register({
|
||||
name: "flipHorizontal",
|
||||
|
@ -50,8 +25,6 @@ export const actionFlipHorizontal = register({
|
|||
},
|
||||
keyTest: (event) => event.shiftKey && event.code === CODES.H,
|
||||
contextItemLabel: "labels.flipHorizontal",
|
||||
predicate: (elements, appState) =>
|
||||
enableActionFlipHorizontal(elements, appState),
|
||||
});
|
||||
|
||||
export const actionFlipVertical = register({
|
||||
|
@ -67,8 +40,6 @@ export const actionFlipVertical = register({
|
|||
keyTest: (event) =>
|
||||
event.shiftKey && event.code === CODES.V && !event[KEYS.CTRL_OR_CMD],
|
||||
contextItemLabel: "labels.flipVertical",
|
||||
predicate: (elements, appState) =>
|
||||
enableActionFlipVertical(elements, appState),
|
||||
});
|
||||
|
||||
const flipSelectedElements = (
|
||||
|
@ -81,11 +52,6 @@ const flipSelectedElements = (
|
|||
appState,
|
||||
);
|
||||
|
||||
// remove once we allow for groups of elements to be flipped
|
||||
if (selectedElements.length > 1) {
|
||||
return elements;
|
||||
}
|
||||
|
||||
const updatedElements = flipElements(
|
||||
selectedElements,
|
||||
appState,
|
||||
|
@ -104,144 +70,20 @@ const flipElements = (
|
|||
appState: AppState,
|
||||
flipDirection: "horizontal" | "vertical",
|
||||
): ExcalidrawElement[] => {
|
||||
elements.forEach((element) => {
|
||||
flipElement(element, appState);
|
||||
// If vertical flip, rotate an extra 180
|
||||
if (flipDirection === "vertical") {
|
||||
rotateElement(element, Math.PI);
|
||||
}
|
||||
});
|
||||
const { minX, minY, maxX, maxY } = getCommonBoundingBox(elements);
|
||||
|
||||
resizeMultipleElements(
|
||||
{ originalElements: arrayToMap(elements) } as PointerDownState,
|
||||
elements,
|
||||
"nw",
|
||||
true,
|
||||
flipDirection === "horizontal" ? maxX : minX,
|
||||
flipDirection === "horizontal" ? minY : maxY,
|
||||
);
|
||||
|
||||
(isBindingEnabled(appState)
|
||||
? bindOrUnbindSelectedElements
|
||||
: unbindLinearElements)(elements);
|
||||
|
||||
return elements;
|
||||
};
|
||||
|
||||
const flipElement = (
|
||||
element: NonDeleted<ExcalidrawElement>,
|
||||
appState: AppState,
|
||||
) => {
|
||||
const originalX = element.x;
|
||||
const originalY = element.y;
|
||||
const width = element.width;
|
||||
const height = element.height;
|
||||
const originalAngle = normalizeAngle(element.angle);
|
||||
|
||||
// Rotate back to zero, if necessary
|
||||
mutateElement(element, {
|
||||
angle: normalizeAngle(0),
|
||||
});
|
||||
// Flip unrotated by pulling TransformHandle to opposite side
|
||||
const transformHandles = getTransformHandles(element, appState.zoom);
|
||||
let usingNWHandle = true;
|
||||
let nHandle = transformHandles.nw;
|
||||
if (!nHandle) {
|
||||
// Use ne handle instead
|
||||
usingNWHandle = false;
|
||||
nHandle = transformHandles.ne;
|
||||
if (!nHandle) {
|
||||
mutateElement(element, {
|
||||
angle: originalAngle,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let finalOffsetX = 0;
|
||||
if (isLinearElement(element) && element.points.length < 3) {
|
||||
finalOffsetX =
|
||||
element.points.reduce((max, point) => Math.max(max, point[0]), 0) * 2 -
|
||||
element.width;
|
||||
}
|
||||
|
||||
let initialPointsCoords;
|
||||
if (isLinearElement(element)) {
|
||||
initialPointsCoords = getElementPointsCoords(element, element.points);
|
||||
}
|
||||
const initialElementAbsoluteCoords = getElementAbsoluteCoords(element);
|
||||
|
||||
if (isLinearElement(element) && element.points.length < 3) {
|
||||
for (let index = 1; index < element.points.length; index++) {
|
||||
LinearElementEditor.movePoints(element, [
|
||||
{
|
||||
index,
|
||||
point: [-element.points[index][0], element.points[index][1]],
|
||||
},
|
||||
]);
|
||||
}
|
||||
LinearElementEditor.normalizePoints(element);
|
||||
} else {
|
||||
const elWidth = initialPointsCoords
|
||||
? initialPointsCoords[2] - initialPointsCoords[0]
|
||||
: initialElementAbsoluteCoords[2] - initialElementAbsoluteCoords[0];
|
||||
|
||||
const startPoint = initialPointsCoords
|
||||
? [initialPointsCoords[0], initialPointsCoords[1]]
|
||||
: [initialElementAbsoluteCoords[0], initialElementAbsoluteCoords[1]];
|
||||
|
||||
resizeSingleElement(
|
||||
new Map().set(element.id, element),
|
||||
false,
|
||||
element,
|
||||
usingNWHandle ? "nw" : "ne",
|
||||
true,
|
||||
usingNWHandle ? startPoint[0] + elWidth : startPoint[0] - elWidth,
|
||||
startPoint[1],
|
||||
);
|
||||
}
|
||||
|
||||
// Rotate by (360 degrees - original angle)
|
||||
let angle = normalizeAngle(2 * Math.PI - originalAngle);
|
||||
if (angle < 0) {
|
||||
// check, probably unnecessary
|
||||
angle = normalizeAngle(angle + 2 * Math.PI);
|
||||
}
|
||||
mutateElement(element, {
|
||||
angle,
|
||||
});
|
||||
|
||||
// Move back to original spot to appear "flipped in place"
|
||||
mutateElement(element, {
|
||||
x: originalX + finalOffsetX,
|
||||
y: originalY,
|
||||
width,
|
||||
height,
|
||||
});
|
||||
|
||||
updateBoundElements(element);
|
||||
|
||||
if (initialPointsCoords && isLinearElement(element)) {
|
||||
// Adjusting origin because when a beizer curve path exceeds min/max points it offsets the origin.
|
||||
// There's still room for improvement since when the line roughness is > 1
|
||||
// we still have a small offset of the origin when fliipping the element.
|
||||
const finalPointsCoords = getElementPointsCoords(element, element.points);
|
||||
|
||||
const topLeftCoordsDiff = initialPointsCoords[0] - finalPointsCoords[0];
|
||||
const topRightCoordDiff = initialPointsCoords[2] - finalPointsCoords[2];
|
||||
|
||||
const coordsDiff = topLeftCoordsDiff + topRightCoordDiff;
|
||||
|
||||
mutateElement(element, {
|
||||
x: element.x + coordsDiff * 0.5,
|
||||
y: element.y,
|
||||
width,
|
||||
height,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const rotateElement = (element: ExcalidrawElement, rotationAngle: number) => {
|
||||
const originalX = element.x;
|
||||
const originalY = element.y;
|
||||
let angle = normalizeAngle(element.angle + rotationAngle);
|
||||
if (angle < 0) {
|
||||
// check, probably unnecessary
|
||||
angle = normalizeAngle(2 * Math.PI + angle);
|
||||
}
|
||||
mutateElement(element, {
|
||||
angle,
|
||||
});
|
||||
|
||||
// Move back to original spot
|
||||
mutateElement(element, {
|
||||
x: originalX,
|
||||
y: originalY,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -119,8 +119,8 @@ const getFormValue = function <T>(
|
|||
elements: readonly ExcalidrawElement[],
|
||||
appState: AppState,
|
||||
getAttribute: (element: ExcalidrawElement) => T,
|
||||
defaultValue?: T,
|
||||
): T | null {
|
||||
defaultValue: T,
|
||||
): T {
|
||||
const editingElement = appState.editingElement;
|
||||
const nonDeletedElements = getNonDeletedElements(elements);
|
||||
return (
|
||||
|
@ -132,7 +132,7 @@ const getFormValue = function <T>(
|
|||
getAttribute,
|
||||
)
|
||||
: defaultValue) ??
|
||||
null
|
||||
defaultValue
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -811,6 +811,7 @@ export const actionChangeTextAlign = register({
|
|||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const actionChangeVerticalAlign = register({
|
||||
name: "changeVerticalAlign",
|
||||
trackEvent: { category: "element" },
|
||||
|
@ -865,16 +866,21 @@ export const actionChangeVerticalAlign = register({
|
|||
testId: "align-bottom",
|
||||
},
|
||||
]}
|
||||
value={getFormValue(elements, appState, (element) => {
|
||||
if (isTextElement(element) && element.containerId) {
|
||||
return element.verticalAlign;
|
||||
}
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
if (boundTextElement) {
|
||||
return boundTextElement.verticalAlign;
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
value={getFormValue(
|
||||
elements,
|
||||
appState,
|
||||
(element) => {
|
||||
if (isTextElement(element) && element.containerId) {
|
||||
return element.verticalAlign;
|
||||
}
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
if (boundTextElement) {
|
||||
return boundTextElement.verticalAlign;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
VERTICAL_ALIGN.MIDDLE,
|
||||
)}
|
||||
onChange={(value) => updateData(value)}
|
||||
/>
|
||||
</fieldset>
|
||||
|
|
|
@ -118,10 +118,13 @@ export class ActionManager {
|
|||
return true;
|
||||
}
|
||||
|
||||
executeAction(action: Action, source: ActionSource = "api") {
|
||||
executeAction(
|
||||
action: Action,
|
||||
source: ActionSource = "api",
|
||||
value: any = null,
|
||||
) {
|
||||
const elements = this.getElementsIncludingDeleted();
|
||||
const appState = this.getAppState();
|
||||
const value = null;
|
||||
|
||||
trackAction(action, source, appState, elements, this.app, value);
|
||||
|
||||
|
|
20
src/assets/lock.svg
Normal file
20
src/assets/lock.svg
Normal file
|
@ -0,0 +1,20 @@
|
|||
<svg width="178" height="162" viewBox="0 0 178 162" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M40.3329 54.3823L38.5547 94.3134L39.7731 111.754L40.1282 118.907L41.0832 123.59L44.3502 131.942L48.9438 137.693L52.5472 143.333L58.5544 147.755L62.5364 150.239L72.3634 154.486L83.15 156.361L91.1212 158.708L101.174 157.525L110.808 156.719L115.983 154.049L124.511 151.377L129.276 148.71L133.701 143.947L139.666 135.877L142.001 128.136L145.746 118.192L145.188 111.065L145.489 94.3675L145.873 75.2546L143.227 59.7779L142.022 47.4695L138.595 46.8345L102.952 45.4703L56.9173 46.7498L46.0719 49.1207L41.9323 50.6825L39.5684 53.4297" fill="#E3E2FE"/>
|
||||
<path d="M41.0014 54.2859C41.0861 64.8796 38.3765 102.581 40.9779 117.876C43.5793 133.17 48.2646 139.346 56.6121 146.047C64.9596 152.746 79.1214 157.662 91.0653 158.078C103.009 158.492 119.347 155.242 128.277 148.543C137.206 141.842 142.112 133.527 144.641 117.874C147.169 102.221 146.061 66.4132 143.446 54.6222C140.83 42.8289 143.97 48.2857 128.948 47.1238C113.925 45.9619 67.9608 46.477 53.3051 47.6483C38.6493 48.8197 43.2053 53.0675 41.0155 54.1518M40.5263 53.9801C40.5404 64.6138 37.9249 103.418 40.5921 118.587C43.257 133.755 48.147 138.325 56.5251 144.991C64.9008 151.655 78.7263 157.935 90.8536 158.577C102.981 159.221 120.212 155.413 129.289 148.844C138.368 142.277 142.872 134.995 145.321 119.168C147.767 103.343 146.805 65.8698 143.977 53.8837C141.148 41.8975 143.615 48.4292 128.348 47.2508C113.081 46.0724 67.14 45.6726 52.3737 46.8157C37.6074 47.9564 41.7776 53.091 39.7524 54.1024" stroke="#6965DB" stroke-width="2" stroke-linecap="round"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M64.7935 45.726L66.36 36.6964L65.5979 34.4501L67.3384 30.7668L70.4197 26.5048L74.8157 21.0598L81.9095 16.9131L89.9419 14.4951L95.6127 12.8534L97.7555 13.2133L103.819 15.269L106.552 18.5807L109.967 22.3793L114.45 27.2387L114.904 34.3937L117.153 38.9214L116.031 44.5005L116.765 45.5448L118.863 47.3559L126.164 47.6782L127.744 46.7797L128.76 44.7052L123.882 25.8227L120.641 20.4835L116.351 15.8758L112.809 11.0729L108.617 7.36601L101.352 5.43025L93.263 3.31104L84.2451 5.1433L77.7299 7.86229L76.0011 8.0975L62.1168 19.8532L59.5366 24.5597L55.7733 32.3215L55.0371 39.5282L56.0626 44.8933L56.5636 47.1725L59.6401 46.8644L66.1648 46.0388" fill="#E3E2FE"/>
|
||||
<path d="M65.0037 46.0106C65.1166 43.8231 64.9237 36.8492 66.1421 33.2412C67.3605 29.6307 69.0799 27.2857 72.314 24.3527C75.5481 21.422 81.273 17.6093 85.5444 15.6453C89.8157 13.6813 94.3317 12.1148 97.9421 12.5664C101.555 13.0157 104.544 15.857 107.219 18.3478C109.893 20.8387 112.356 24.0869 113.986 27.5115C115.618 30.9338 116.389 35.8684 117.006 38.8861C117.622 41.9038 115.992 44.2888 117.69 45.6178C119.388 46.949 125.617 48.1721 127.195 46.8667C128.773 45.5613 127.717 41.1888 127.157 37.7877C126.597 34.3866 125.494 30.1247 123.838 26.4601C122.183 22.7956 119.746 18.9029 117.222 15.7982C114.698 12.6934 112.791 9.9086 108.696 7.83642C104.601 5.76189 97.8081 3.42863 92.6547 3.35337C87.5013 3.2781 81.5529 5.74308 77.7708 7.38717C73.991 9.03127 72.8879 10.6166 69.9666 13.218C67.043 15.8217 62.4306 19.6768 60.2384 23.0026C58.0486 26.3284 57.4818 29.252 56.8185 33.1753C56.1529 37.0962 54.6499 44.39 56.2517 46.5327C57.8511 48.6731 64.7756 45.98 66.4267 46.0247M65.9704 45.5096C65.9845 43.348 64.2652 37.5525 65.5423 33.8456C66.8172 30.1364 70.2959 26.4789 73.6264 23.259C76.9546 20.039 81.3177 16.3015 85.5208 14.5281C89.7216 12.7523 95.1079 11.7903 98.8383 12.6111C102.569 13.432 105.283 16.8072 107.903 19.4486C110.526 22.09 113.146 25.3029 114.567 28.4664C115.987 31.6276 116.03 35.4051 116.425 38.4204C116.82 41.4334 115.124 45.1426 116.937 46.5515C118.751 47.9604 125.539 48.2968 127.31 46.8761C129.081 45.4578 127.978 41.4428 127.562 38.0347C127.145 34.6265 126.501 30.1646 124.81 26.4296C123.116 22.6921 120.195 18.7594 117.413 15.6194C114.63 12.4818 112.247 9.39349 108.117 7.58945C103.987 5.78541 97.5776 5.02099 92.6335 4.79519C87.6895 4.56939 82.3503 4.78813 78.4505 6.23466C74.5484 7.68118 72.0882 10.6542 69.228 13.4696C66.3679 16.2851 63.4725 19.7873 61.2898 23.1319C59.1071 26.4789 56.9761 29.4896 56.1293 33.5469C55.285 37.6043 54.577 45.2132 56.2117 47.4759C57.8487 49.7409 64.2675 47.3418 65.9445 47.1301" stroke="#6965DB" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M140.37 54.8958C137.884 58.1322 127.704 71.2286 125.185 74.5427M139.697 54.209C137.098 57.5466 127.005 71.7884 124.51 75.3565" stroke="#6965DB" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M141.663 63.1765C139.661 66.0413 131.311 77.1501 129.077 79.9726M141.065 62.5908C139.021 65.2792 130.631 76.1364 128.717 78.8625" stroke="#6965DB" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M141.888 72.9917C139.475 75.8589 130.268 86.8478 127.966 89.7455M141.02 72.726C138.503 75.6496 129.775 87.2476 127.58 90.3242" stroke="#6965DB" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M141.948 82.215C139.815 85.1057 130.308 96.8214 127.961 99.7709M141.459 81.7375C139.298 84.4119 129.816 95.9888 127.479 98.8606" stroke="#6965DB" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M141.357 91.7838C138.885 95.2484 128.808 108.535 126.428 111.76M142.474 91.4757C139.917 94.7921 128.38 107.493 125.781 110.883" stroke="#6965DB" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M142.568 101.479C140.028 104.403 129.867 115.528 127.195 118.356M141.811 101.018C139.212 104.055 129.477 115.975 126.828 118.97" stroke="#6965DB" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M141.023 112.172C138.591 114.775 128.028 125.905 125.422 128.664M140.51 113.465C138.008 116.147 127.36 125.233 124.742 127.582" stroke="#6965DB" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M139.004 123.69C136.501 126.275 125.952 137.248 123.287 140.108M138.343 124.817C135.805 127.454 125.487 138.261 122.848 140.75" stroke="#6965DB" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M132.192 139.862C129.854 141.624 120.87 148.168 118.574 150.012M131.39 139.496C128.97 141.333 120.524 148.89 118.322 150.621" stroke="#6965DB" stroke-width="2" stroke-linecap="round"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M82.6351 92.3124L78.2767 89.0148L78.6718 88.8784L75.6282 79.6865L74.4922 76.0525L75.0379 74.1074L78.6248 69.5444L83.6182 65.186L86.6924 64.0711L93.7768 63.9864L99.9181 63.9276L103.905 64.4215L106.038 66.068L109.333 67.6392L110.251 69.4479L112.438 73.1877L112.702 81.928L111.674 82.93L110.907 85.5573L107.828 89.2336L101.273 92.9193L102.785 120.401L99.5488 125.521L98.0059 127.838L96.1313 129.414L93.17 130.237L92.2198 130.033L90.1358 129.233L88.8328 126.594L87.8378 95.2549L88.9386 93.3215L86.0409 91.294L80.9533 91.1552" fill="white"/>
|
||||
<path d="M82.8214 92.0607C82.0664 91.4327 79.291 90.7201 77.8539 88.2033C76.4167 85.6866 73.5284 80.4438 74.1964 76.9581C74.862 73.4723 78.6959 69.6384 81.8524 67.2887C85.0089 64.939 88.9227 63.1138 93.1353 62.8574C97.3478 62.6034 103.957 63.9888 107.132 65.7575C110.31 67.5263 111.416 70.5651 112.196 73.4747C112.977 76.3842 112.606 80.6626 111.82 83.2122C111.035 85.7642 109.078 87.1661 107.481 88.7749C105.883 90.3837 103.106 91.2751 102.233 92.8651C101.363 94.4551 102.327 95.3254 102.25 98.3125C102.172 101.3 101.76 107.227 101.767 110.788C101.772 114.349 102.487 116.981 102.285 119.676C102.085 122.374 101.52 125.126 100.556 126.965C99.5917 128.805 98.077 130.256 96.5011 130.715C94.9275 131.171 92.4485 130.36 91.1101 129.713C89.7742 129.066 89.0144 128.341 88.4805 126.836C87.9489 125.331 87.9678 123.964 87.9137 120.681C87.8596 117.397 88.1159 111.599 88.1583 107.14C88.203 102.68 89.2779 96.445 88.1724 93.9236C87.0693 91.4022 82.7791 92.4347 81.5325 92.0137M82.0194 91.6068C81.222 90.7624 78.4536 89.7886 77.3623 87.1567C76.2733 84.5247 74.6621 79.3125 75.4783 75.815C76.2921 72.3151 79.1428 68.3166 82.2522 66.1597C85.3617 64.0029 90.1693 63.062 94.1302 62.8739C98.0911 62.6857 102.925 63.0832 106.02 65.0331C109.118 66.9853 111.834 71.5836 112.705 74.5801C113.572 77.5743 111.949 80.7731 111.234 83.0076C110.519 85.2444 109.835 86.3711 108.417 87.9916C107.001 89.6146 103.738 90.9623 102.732 92.7358C101.725 94.5092 102.351 95.6382 102.377 98.6301C102.405 101.62 102.866 106.949 102.894 110.682C102.922 114.417 102.955 118.291 102.544 121.038C102.13 123.783 101.408 125.54 100.42 127.161C99.4318 128.781 98.1005 130.233 96.6163 130.759C95.1322 131.286 92.9353 130.893 91.51 130.322C90.0846 129.753 88.7769 128.889 88.0618 127.335C87.3468 125.78 87.0128 124.317 87.2198 120.998C87.4268 117.68 89.0874 112.046 89.299 107.422C89.5107 102.798 89.8494 95.9322 88.4946 93.2509C87.1398 90.5695 82.4804 91.4845 81.1679 91.3316" stroke="#6965DB" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M28.1943 139.31C26.7936 139.432 25.332 140.402 23.8703 140.523C23.1395 140.766 22.5914 140.16 21.9824 139.735C21.5561 139.553 21.008 138.461 20.8253 138.219C20.5817 136.884 19.9118 134.276 20.0336 133.002C19.7291 131.364 21.5561 129.787 23.0786 129.727C23.2613 129.727 23.8094 129.787 23.8703 129.787C25.7583 130.151 27.5853 131.546 29.5341 131.728C29.595 131.728 29.6559 131.728 29.6559 131.668C30.4476 130.333 30.204 126.937 30.813 125.542C30.813 125.36 31.1784 123.54 31.1784 123.237C31.6048 122.327 32.1529 121.781 33.1273 122.084C33.7972 122.266 34.6498 122.388 34.6498 123.237V128.635C34.8325 129.242 36.1114 128.999 36.5986 128.999C38.7911 128.028 40.8617 127.422 43.3586 127.058C45.6729 127.179 46.7082 129.242 46.5864 131.304C46.6473 132.396 45.4293 133.245 44.6985 133.973C44.4549 134.094 43.4804 134.519 43.115 134.397C42.2624 133.791 41.1662 134.033 40.1309 134.094C40.1309 134.155 40.0091 134.337 40.07 134.397C41.288 135.853 43.5413 136.096 45.0639 137.066C46.1601 138.34 47.4999 138.643 47.1345 140.341C47.0736 141.191 47.1345 142.1 46.221 142.404C45.9774 142.586 44.5767 142.828 44.2722 142.828C43.9677 142.768 43.3586 142.343 43.115 142.04C40.9835 141.13 38.6693 140.402 36.2332 140.159V145.133C35.9896 146.468 35.6851 147.923 34.6498 148.955C34.2844 149.015 33.1273 149.015 32.7619 148.955C32.4574 148.773 31.4221 147.741 31.1784 147.438C30.5694 145.133 30.4476 142.404 29.6559 140.159C29.1687 139.553 28.986 139.25 28.1943 139.31Z" fill="#6965DB"/>
|
||||
<path d="M59.5964 139.31C58.1956 139.432 56.734 140.402 55.2724 140.523C54.5416 140.766 53.9935 140.16 53.3845 139.735C52.9582 139.553 52.41 138.461 52.2273 138.219C51.9837 136.884 51.3138 134.276 51.4356 133.002C51.1311 131.364 52.9582 129.787 54.4807 129.727C54.6634 129.727 55.2115 129.787 55.2724 129.787C57.1603 130.151 58.9874 131.546 60.9362 131.728C60.9971 131.728 61.058 131.728 61.058 131.668C61.8497 130.333 61.6061 126.937 62.2151 125.542C62.2151 125.36 62.5805 123.54 62.5805 123.237C63.0068 122.327 63.5549 121.781 64.5293 122.084C65.1992 122.266 66.0519 122.388 66.0519 123.237V128.635C66.2346 129.242 67.5135 128.999 68.0007 128.999C70.1931 128.028 72.2638 127.422 74.7607 127.058C77.0749 127.179 78.1103 129.242 77.9885 131.304C78.0494 132.396 76.8313 133.245 76.1005 133.973C75.8569 134.094 74.8825 134.519 74.5171 134.397C73.6645 133.791 72.5683 134.033 71.5329 134.094C71.5329 134.155 71.4112 134.337 71.4721 134.397C72.6901 135.853 74.9434 136.096 76.4659 137.066C77.5621 138.34 78.902 138.643 78.5366 140.341C78.4757 141.191 78.5366 142.1 77.623 142.404C77.3794 142.586 75.9787 142.828 75.6742 142.828C75.3697 142.768 74.7607 142.343 74.5171 142.04C72.3856 141.13 70.0713 140.402 67.6353 140.159V145.133C67.3917 146.468 67.0872 147.923 66.0519 148.955C65.6865 149.015 64.5293 149.015 64.1639 148.955C63.8594 148.773 62.8241 147.741 62.5805 147.438C61.9715 145.133 61.8497 142.404 61.058 140.159C60.5708 139.553 60.3881 139.25 59.5964 139.31Z" fill="#6965DB"/>
|
||||
<path d="M90.9984 139.31C89.5977 139.432 88.1361 140.402 86.6745 140.523C85.9436 140.766 85.3955 140.16 84.7865 139.735C84.3602 139.553 83.8121 138.461 83.6294 138.219C83.3858 136.884 82.7159 134.276 82.8377 133.002C82.5332 131.364 84.3602 129.787 85.8827 129.727C86.0654 129.727 86.6136 129.787 86.6745 129.787C88.5624 130.151 90.3894 131.546 92.3382 131.728C92.3991 131.728 92.46 131.728 92.46 131.668C93.2518 130.333 93.0082 126.937 93.6172 125.542C93.6172 125.36 93.9826 123.54 93.9826 123.237C94.4089 122.327 94.957 121.781 95.9314 122.084C96.6013 122.266 97.4539 122.388 97.4539 123.237V128.635C97.6366 129.242 98.9155 128.999 99.4028 128.999C101.595 128.028 103.666 127.422 106.163 127.058C108.477 127.179 109.512 129.242 109.391 131.304C109.451 132.396 108.233 133.245 107.503 133.973C107.259 134.094 106.285 134.519 105.919 134.397C105.067 133.791 103.97 134.033 102.935 134.094C102.935 134.155 102.813 134.337 102.874 134.397C104.092 135.853 106.345 136.096 107.868 137.066C108.964 138.34 110.304 138.643 109.939 140.341C109.878 141.191 109.939 142.1 109.025 142.404C108.782 142.586 107.381 142.828 107.076 142.828C106.772 142.768 106.163 142.343 105.919 142.04C103.788 141.13 101.473 140.402 99.0373 140.159V145.133C98.7937 146.468 98.4892 147.923 97.4539 148.955C97.0885 149.015 95.9314 149.015 95.566 148.955C95.2615 148.773 94.2262 147.741 93.9826 147.438C93.3736 145.133 93.2518 142.404 92.46 140.159C91.9728 139.553 91.7901 139.25 90.9984 139.31Z" fill="#6965DB"/>
|
||||
</svg>
|
After Width: | Height: | Size: 13 KiB |
|
@ -164,4 +164,7 @@ export const getAllColorsSpecificShade = (index: 0 | 1 | 2 | 3 | 4) =>
|
|||
COLOR_PALETTE.red[index],
|
||||
] as const;
|
||||
|
||||
export const rgbToHex = (r: number, g: number, b: number) =>
|
||||
`#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
|
|
@ -305,6 +305,7 @@ import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
|
|||
import { actionWrapTextInContainer } from "../actions/actionBoundText";
|
||||
import BraveMeasureTextError from "./BraveMeasureTextError";
|
||||
import { convertToExcalidrawElements } from "../data/transform";
|
||||
import { activeEyeDropperAtom } from "./EyeDropper";
|
||||
|
||||
const AppContext = React.createContext<AppClassProperties>(null!);
|
||||
const AppPropsContext = React.createContext<AppProps>(null!);
|
||||
|
@ -367,8 +368,6 @@ export const useExcalidrawActionManager = () =>
|
|||
|
||||
let didTapTwice: boolean = false;
|
||||
let tappedTwiceTimer = 0;
|
||||
let cursorX = 0;
|
||||
let cursorY = 0;
|
||||
let isHoldingSpace: boolean = false;
|
||||
let isPanning: boolean = false;
|
||||
let isDraggingScrollBar: boolean = false;
|
||||
|
@ -426,7 +425,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
hitLinkElement?: NonDeletedExcalidrawElement;
|
||||
lastPointerDown: React.PointerEvent<HTMLCanvasElement> | null = null;
|
||||
lastPointerUp: React.PointerEvent<HTMLElement> | PointerEvent | null = null;
|
||||
lastScenePointer: { x: number; y: number } | null = null;
|
||||
lastViewportPosition = { x: 0, y: 0 };
|
||||
|
||||
constructor(props: AppProps) {
|
||||
super(props);
|
||||
|
@ -635,6 +634,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
</LayerUI>
|
||||
<div className="excalidraw-textEditorContainer" />
|
||||
<div className="excalidraw-contextMenuContainer" />
|
||||
<div className="excalidraw-eye-dropper-container" />
|
||||
{selectedElement.length === 1 &&
|
||||
!this.state.contextMenu &&
|
||||
this.state.showHyperlinkPopup && (
|
||||
|
@ -725,6 +725,49 @@ class App extends React.Component<AppProps, AppState> {
|
|||
}
|
||||
};
|
||||
|
||||
private openEyeDropper = ({ type }: { type: "stroke" | "background" }) => {
|
||||
jotaiStore.set(activeEyeDropperAtom, {
|
||||
swapPreviewOnAlt: true,
|
||||
previewType: type === "stroke" ? "strokeColor" : "backgroundColor",
|
||||
onSelect: (color, event) => {
|
||||
const shouldUpdateStrokeColor =
|
||||
(type === "background" && event.altKey) ||
|
||||
(type === "stroke" && !event.altKey);
|
||||
const selectedElements = getSelectedElements(
|
||||
this.scene.getElementsIncludingDeleted(),
|
||||
this.state,
|
||||
);
|
||||
if (
|
||||
!selectedElements.length ||
|
||||
this.state.activeTool.type !== "selection"
|
||||
) {
|
||||
if (shouldUpdateStrokeColor) {
|
||||
this.setState({
|
||||
currentItemStrokeColor: color,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
currentItemBackgroundColor: color,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.updateScene({
|
||||
elements: this.scene.getElementsIncludingDeleted().map((el) => {
|
||||
if (this.state.selectedElementIds[el.id]) {
|
||||
return newElementWith(el, {
|
||||
[shouldUpdateStrokeColor ? "strokeColor" : "backgroundColor"]:
|
||||
color,
|
||||
});
|
||||
}
|
||||
return el;
|
||||
}),
|
||||
});
|
||||
}
|
||||
},
|
||||
keepOpenOnAlt: false,
|
||||
});
|
||||
};
|
||||
|
||||
private syncActionResult = withBatchedUpdates(
|
||||
(actionResult: ActionResult) => {
|
||||
if (this.unmounted || actionResult === false) {
|
||||
|
@ -1073,6 +1116,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
this.unmounted = true;
|
||||
this.removeEventListeners();
|
||||
this.scene.destroy();
|
||||
this.library.destroy();
|
||||
clearTimeout(touchTimeout);
|
||||
touchTimeout = 0;
|
||||
}
|
||||
|
@ -1569,7 +1613,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||
return;
|
||||
}
|
||||
|
||||
const elementUnderCursor = document.elementFromPoint(cursorX, cursorY);
|
||||
const elementUnderCursor = document.elementFromPoint(
|
||||
this.lastViewportPosition.x,
|
||||
this.lastViewportPosition.y,
|
||||
);
|
||||
if (
|
||||
event &&
|
||||
(!(elementUnderCursor instanceof HTMLCanvasElement) ||
|
||||
|
@ -1596,7 +1643,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||
// prefer spreadsheet data over image file (MS Office/Libre Office)
|
||||
if (isSupportedImageFile(file) && !data.spreadsheet) {
|
||||
const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
|
||||
{ clientX: cursorX, clientY: cursorY },
|
||||
{
|
||||
clientX: this.lastViewportPosition.x,
|
||||
clientY: this.lastViewportPosition.y,
|
||||
},
|
||||
this.state,
|
||||
);
|
||||
|
||||
|
@ -1659,13 +1709,13 @@ class App extends React.Component<AppProps, AppState> {
|
|||
typeof opts.position === "object"
|
||||
? opts.position.clientX
|
||||
: opts.position === "cursor"
|
||||
? cursorX
|
||||
? this.lastViewportPosition.x
|
||||
: this.state.width / 2 + this.state.offsetLeft;
|
||||
const clientY =
|
||||
typeof opts.position === "object"
|
||||
? opts.position.clientY
|
||||
: opts.position === "cursor"
|
||||
? cursorY
|
||||
? this.lastViewportPosition.y
|
||||
: this.state.height / 2 + this.state.offsetTop;
|
||||
|
||||
const { x, y } = viewportCoordsToSceneCoords(
|
||||
|
@ -1749,7 +1799,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||
|
||||
private addTextFromPaste(text: string, isPlainPaste = false) {
|
||||
const { x, y } = viewportCoordsToSceneCoords(
|
||||
{ clientX: cursorX, clientY: cursorY },
|
||||
{
|
||||
clientX: this.lastViewportPosition.x,
|
||||
clientY: this.lastViewportPosition.y,
|
||||
},
|
||||
this.state,
|
||||
);
|
||||
|
||||
|
@ -2084,8 +2137,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||
|
||||
private updateCurrentCursorPosition = withBatchedUpdates(
|
||||
(event: MouseEvent) => {
|
||||
cursorX = event.clientX;
|
||||
cursorY = event.clientY;
|
||||
this.lastViewportPosition.x = event.clientX;
|
||||
this.lastViewportPosition.y = event.clientY;
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -2158,6 +2211,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
event.shiftKey &&
|
||||
event[KEYS.CTRL_OR_CMD]
|
||||
) {
|
||||
event.preventDefault();
|
||||
this.setState({ openDialog: "imageExport" });
|
||||
return;
|
||||
}
|
||||
|
@ -2342,6 +2396,20 @@ class App extends React.Component<AppProps, AppState> {
|
|||
) {
|
||||
jotaiStore.set(activeConfirmDialogAtom, "clearCanvas");
|
||||
}
|
||||
|
||||
// eye dropper
|
||||
// -----------------------------------------------------------------------
|
||||
const lowerCased = event.key.toLocaleLowerCase();
|
||||
const isPickingStroke = lowerCased === KEYS.S && event.shiftKey;
|
||||
const isPickingBackground =
|
||||
event.key === KEYS.I || (lowerCased === KEYS.G && event.shiftKey);
|
||||
|
||||
if (isPickingStroke || isPickingBackground) {
|
||||
this.openEyeDropper({
|
||||
type: isPickingStroke ? "stroke" : "background",
|
||||
});
|
||||
}
|
||||
// -----------------------------------------------------------------------
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -2471,8 +2539,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||
this.setState((state) => ({
|
||||
...getStateForZoom(
|
||||
{
|
||||
viewportX: cursorX,
|
||||
viewportY: cursorY,
|
||||
viewportX: this.lastViewportPosition.x,
|
||||
viewportY: this.lastViewportPosition.y,
|
||||
nextZoom: getNormalizedZoom(initialScale * event.scale),
|
||||
},
|
||||
state,
|
||||
|
@ -6468,8 +6536,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||
this.translateCanvas((state) => ({
|
||||
...getStateForZoom(
|
||||
{
|
||||
viewportX: cursorX,
|
||||
viewportY: cursorY,
|
||||
viewportX: this.lastViewportPosition.x,
|
||||
viewportY: this.lastViewportPosition.y,
|
||||
nextZoom: getNormalizedZoom(newZoom),
|
||||
},
|
||||
state,
|
||||
|
|
|
@ -2,15 +2,23 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
|||
import { getColor } from "./ColorPicker";
|
||||
import { useAtom } from "jotai";
|
||||
import { activeColorPickerSectionAtom } from "./colorPickerUtils";
|
||||
import { eyeDropperIcon } from "../icons";
|
||||
import { jotaiScope } from "../../jotai";
|
||||
import { KEYS } from "../../keys";
|
||||
import { activeEyeDropperAtom } from "../EyeDropper";
|
||||
import clsx from "clsx";
|
||||
import { t } from "../../i18n";
|
||||
import { useDevice } from "../App";
|
||||
import { getShortcutKey } from "../../utils";
|
||||
|
||||
interface ColorInputProps {
|
||||
color: string | null;
|
||||
color: string;
|
||||
onChange: (color: string) => void;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export const ColorInput = ({ color, onChange, label }: ColorInputProps) => {
|
||||
const device = useDevice();
|
||||
const [innerValue, setInnerValue] = useState(color);
|
||||
const [activeSection, setActiveColorPickerSection] = useAtom(
|
||||
activeColorPickerSectionAtom,
|
||||
|
@ -34,7 +42,7 @@ export const ColorInput = ({ color, onChange, label }: ColorInputProps) => {
|
|||
);
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const divRef = useRef<HTMLDivElement>(null);
|
||||
const eyeDropperTriggerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef.current) {
|
||||
|
@ -42,8 +50,19 @@ export const ColorInput = ({ color, onChange, label }: ColorInputProps) => {
|
|||
}
|
||||
}, [activeSection]);
|
||||
|
||||
const [eyeDropperState, setEyeDropperState] = useAtom(
|
||||
activeEyeDropperAtom,
|
||||
jotaiScope,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
setEyeDropperState(null);
|
||||
};
|
||||
}, [setEyeDropperState]);
|
||||
|
||||
return (
|
||||
<label className="color-picker__input-label">
|
||||
<div className="color-picker__input-label">
|
||||
<div className="color-picker__input-hash">#</div>
|
||||
<input
|
||||
ref={activeSection === "hex" ? inputRef : undefined}
|
||||
|
@ -60,16 +79,48 @@ export const ColorInput = ({ color, onChange, label }: ColorInputProps) => {
|
|||
}}
|
||||
tabIndex={-1}
|
||||
onFocus={() => setActiveColorPickerSection("hex")}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === KEYS.TAB) {
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === KEYS.TAB) {
|
||||
return;
|
||||
} else if (event.key === KEYS.ESCAPE) {
|
||||
eyeDropperTriggerRef.current?.focus();
|
||||
}
|
||||
if (e.key === KEYS.ESCAPE) {
|
||||
divRef.current?.focus();
|
||||
}
|
||||
e.stopPropagation();
|
||||
event.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
{/* TODO reenable on mobile with a better UX */}
|
||||
{!device.isMobile && (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
width: "1px",
|
||||
height: "1.25rem",
|
||||
backgroundColor: "var(--default-border-color)",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
ref={eyeDropperTriggerRef}
|
||||
className={clsx("excalidraw-eye-dropper-trigger", {
|
||||
selected: eyeDropperState,
|
||||
})}
|
||||
onClick={() =>
|
||||
setEyeDropperState((s) =>
|
||||
s
|
||||
? null
|
||||
: {
|
||||
keepOpenOnAlt: false,
|
||||
onSelect: (color) => onChange(color),
|
||||
},
|
||||
)
|
||||
}
|
||||
title={`${t(
|
||||
"labels.eyeDropper",
|
||||
)} — ${KEYS.I.toLocaleUpperCase()} or ${getShortcutKey("Alt")} `}
|
||||
>
|
||||
{eyeDropperIcon}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -204,6 +204,7 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.color-picker-content--default {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { isTransparent } from "../../utils";
|
||||
import { isTransparent, isWritableElement } from "../../utils";
|
||||
import { ExcalidrawElement } from "../../element/types";
|
||||
import { AppState } from "../../types";
|
||||
import { TopPicks } from "./TopPicks";
|
||||
|
@ -12,12 +12,14 @@ import {
|
|||
import { useDevice, useExcalidrawContainer } from "../App";
|
||||
import { ColorTuple, COLOR_PALETTE, ColorPaletteCustom } from "../../colors";
|
||||
import PickerHeading from "./PickerHeading";
|
||||
import { ColorInput } from "./ColorInput";
|
||||
import { t } from "../../i18n";
|
||||
import clsx from "clsx";
|
||||
import { jotaiScope } from "../../jotai";
|
||||
import { ColorInput } from "./ColorInput";
|
||||
import { useRef } from "react";
|
||||
import { activeEyeDropperAtom } from "../EyeDropper";
|
||||
|
||||
import "./ColorPicker.scss";
|
||||
import React from "react";
|
||||
|
||||
const isValidColor = (color: string) => {
|
||||
const style = new Option().style;
|
||||
|
@ -40,9 +42,9 @@ export const getColor = (color: string): string | null => {
|
|||
: null;
|
||||
};
|
||||
|
||||
export interface ColorPickerProps {
|
||||
interface ColorPickerProps {
|
||||
type: ColorPickerType;
|
||||
color: string | null;
|
||||
color: string;
|
||||
onChange: (color: string) => void;
|
||||
label: string;
|
||||
elements: readonly ExcalidrawElement[];
|
||||
|
@ -66,13 +68,17 @@ const ColorPickerPopupContent = ({
|
|||
| "color"
|
||||
| "onChange"
|
||||
| "label"
|
||||
| "label"
|
||||
| "elements"
|
||||
| "palette"
|
||||
| "updateData"
|
||||
>) => {
|
||||
const [, setActiveColorPickerSection] = useAtom(activeColorPickerSectionAtom);
|
||||
|
||||
const [eyeDropperState, setEyeDropperState] = useAtom(
|
||||
activeEyeDropperAtom,
|
||||
jotaiScope,
|
||||
);
|
||||
|
||||
const { container } = useExcalidrawContainer();
|
||||
const { isMobile, isLandscape } = useDevice();
|
||||
|
||||
|
@ -88,21 +94,42 @@ const ColorPickerPopupContent = ({
|
|||
/>
|
||||
</div>
|
||||
);
|
||||
const popoverRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const focusPickerContent = () => {
|
||||
popoverRef.current
|
||||
?.querySelector<HTMLDivElement>(".color-picker-content")
|
||||
?.focus();
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover.Portal container={container}>
|
||||
<Popover.Content
|
||||
ref={popoverRef}
|
||||
className="focus-visible-none"
|
||||
data-prevent-outside-click
|
||||
onFocusOutside={(event) => {
|
||||
focusPickerContent();
|
||||
event.preventDefault();
|
||||
}}
|
||||
onPointerDownOutside={(event) => {
|
||||
if (eyeDropperState) {
|
||||
// prevent from closing if we click outside the popover
|
||||
// while eyedropping (e.g. click when clicking the sidebar;
|
||||
// the eye-dropper-backdrop is prevented downstream)
|
||||
event.preventDefault();
|
||||
}
|
||||
}}
|
||||
onCloseAutoFocus={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// return focus to excalidraw container
|
||||
if (container) {
|
||||
container.focus();
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
updateData({ openPopup: null });
|
||||
setActiveColorPickerSection(null);
|
||||
}}
|
||||
side={isMobile && !isLandscape ? "bottom" : "right"}
|
||||
|
@ -125,10 +152,38 @@ const ColorPickerPopupContent = ({
|
|||
{palette ? (
|
||||
<Picker
|
||||
palette={palette}
|
||||
color={color || null}
|
||||
color={color}
|
||||
onChange={(changedColor) => {
|
||||
onChange(changedColor);
|
||||
}}
|
||||
onEyeDropperToggle={(force) => {
|
||||
setEyeDropperState((state) => {
|
||||
if (force) {
|
||||
state = state || {
|
||||
keepOpenOnAlt: true,
|
||||
onSelect: onChange,
|
||||
};
|
||||
state.keepOpenOnAlt = true;
|
||||
return state;
|
||||
}
|
||||
|
||||
return force === false || state
|
||||
? null
|
||||
: {
|
||||
keepOpenOnAlt: false,
|
||||
onSelect: onChange,
|
||||
};
|
||||
});
|
||||
}}
|
||||
onEscape={(event) => {
|
||||
if (eyeDropperState) {
|
||||
setEyeDropperState(null);
|
||||
} else if (isWritableElement(event.target)) {
|
||||
focusPickerContent();
|
||||
} else {
|
||||
updateData({ openPopup: null });
|
||||
}
|
||||
}}
|
||||
label={label}
|
||||
type={type}
|
||||
elements={elements}
|
||||
|
@ -157,7 +212,7 @@ const ColorPickerTrigger = ({
|
|||
color,
|
||||
type,
|
||||
}: {
|
||||
color: string | null;
|
||||
color: string;
|
||||
label: string;
|
||||
type: ColorPickerType;
|
||||
}) => {
|
||||
|
|
|
@ -6,7 +6,7 @@ import HotkeyLabel from "./HotkeyLabel";
|
|||
|
||||
interface CustomColorListProps {
|
||||
colors: string[];
|
||||
color: string | null;
|
||||
color: string;
|
||||
onChange: (color: string) => void;
|
||||
label: string;
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ import PickerHeading from "./PickerHeading";
|
|||
import {
|
||||
ColorPickerType,
|
||||
activeColorPickerSectionAtom,
|
||||
getColorNameAndShadeFromHex,
|
||||
getColorNameAndShadeFromColor,
|
||||
getMostUsedCustomColors,
|
||||
isCustomColor,
|
||||
} from "./colorPickerUtils";
|
||||
|
@ -21,9 +21,11 @@ import {
|
|||
DEFAULT_ELEMENT_BACKGROUND_COLOR_INDEX,
|
||||
DEFAULT_ELEMENT_STROKE_COLOR_INDEX,
|
||||
} from "../../colors";
|
||||
import { KEYS } from "../../keys";
|
||||
import { EVENT } from "../../constants";
|
||||
|
||||
interface PickerProps {
|
||||
color: string | null;
|
||||
color: string;
|
||||
onChange: (color: string) => void;
|
||||
label: string;
|
||||
type: ColorPickerType;
|
||||
|
@ -31,6 +33,8 @@ interface PickerProps {
|
|||
palette: ColorPaletteCustom;
|
||||
updateData: (formData?: any) => void;
|
||||
children?: React.ReactNode;
|
||||
onEyeDropperToggle: (force?: boolean) => void;
|
||||
onEscape: (event: React.KeyboardEvent | KeyboardEvent) => void;
|
||||
}
|
||||
|
||||
export const Picker = ({
|
||||
|
@ -42,6 +46,8 @@ export const Picker = ({
|
|||
palette,
|
||||
updateData,
|
||||
children,
|
||||
onEyeDropperToggle,
|
||||
onEscape,
|
||||
}: PickerProps) => {
|
||||
const [customColors] = React.useState(() => {
|
||||
if (type === "canvasBackground") {
|
||||
|
@ -54,16 +60,15 @@ export const Picker = ({
|
|||
activeColorPickerSectionAtom,
|
||||
);
|
||||
|
||||
const colorObj = getColorNameAndShadeFromHex({
|
||||
hex: color || "transparent",
|
||||
const colorObj = getColorNameAndShadeFromColor({
|
||||
color,
|
||||
palette,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeColorPickerSection) {
|
||||
const isCustom = isCustomColor({ color, palette });
|
||||
const isCustomButNotInList =
|
||||
isCustom && !customColors.includes(color || "");
|
||||
const isCustomButNotInList = isCustom && !customColors.includes(color);
|
||||
|
||||
setActiveColorPickerSection(
|
||||
isCustomButNotInList
|
||||
|
@ -95,26 +100,43 @@ export const Picker = ({
|
|||
if (colorObj?.shade != null) {
|
||||
setActiveShade(colorObj.shade);
|
||||
}
|
||||
}, [colorObj]);
|
||||
|
||||
const keyup = (event: KeyboardEvent) => {
|
||||
if (event.key === KEYS.ALT) {
|
||||
onEyeDropperToggle(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener(EVENT.KEYUP, keyup, { capture: true });
|
||||
return () => {
|
||||
document.removeEventListener(EVENT.KEYUP, keyup, { capture: true });
|
||||
};
|
||||
}, [colorObj, onEyeDropperToggle]);
|
||||
|
||||
const pickerRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
<div role="dialog" aria-modal="true" aria-label={t("labels.colorPicker")}>
|
||||
<div
|
||||
onKeyDown={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
colorPickerKeyNavHandler({
|
||||
e,
|
||||
ref={pickerRef}
|
||||
onKeyDown={(event) => {
|
||||
const handled = colorPickerKeyNavHandler({
|
||||
event,
|
||||
activeColorPickerSection,
|
||||
palette,
|
||||
hex: color,
|
||||
color,
|
||||
onChange,
|
||||
onEyeDropperToggle,
|
||||
customColors,
|
||||
setActiveColorPickerSection,
|
||||
updateData,
|
||||
activeShade,
|
||||
onEscape,
|
||||
});
|
||||
|
||||
if (handled) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
}}
|
||||
className="color-picker-content"
|
||||
// to allow focusing by clicking but not by tabbing
|
||||
|
|
|
@ -4,7 +4,7 @@ import { useEffect, useRef } from "react";
|
|||
import {
|
||||
activeColorPickerSectionAtom,
|
||||
colorPickerHotkeyBindings,
|
||||
getColorNameAndShadeFromHex,
|
||||
getColorNameAndShadeFromColor,
|
||||
} from "./colorPickerUtils";
|
||||
import HotkeyLabel from "./HotkeyLabel";
|
||||
import { ColorPaletteCustom } from "../../colors";
|
||||
|
@ -12,7 +12,7 @@ import { t } from "../../i18n";
|
|||
|
||||
interface PickerColorListProps {
|
||||
palette: ColorPaletteCustom;
|
||||
color: string | null;
|
||||
color: string;
|
||||
onChange: (color: string) => void;
|
||||
label: string;
|
||||
activeShade: number;
|
||||
|
@ -25,8 +25,8 @@ const PickerColorList = ({
|
|||
label,
|
||||
activeShade,
|
||||
}: PickerColorListProps) => {
|
||||
const colorObj = getColorNameAndShadeFromHex({
|
||||
hex: color || "transparent",
|
||||
const colorObj = getColorNameAndShadeFromColor({
|
||||
color: color || "transparent",
|
||||
palette,
|
||||
});
|
||||
const [activeColorPickerSection, setActiveColorPickerSection] = useAtom(
|
||||
|
|
|
@ -3,21 +3,21 @@ import { useAtom } from "jotai";
|
|||
import { useEffect, useRef } from "react";
|
||||
import {
|
||||
activeColorPickerSectionAtom,
|
||||
getColorNameAndShadeFromHex,
|
||||
getColorNameAndShadeFromColor,
|
||||
} from "./colorPickerUtils";
|
||||
import HotkeyLabel from "./HotkeyLabel";
|
||||
import { t } from "../../i18n";
|
||||
import { ColorPaletteCustom } from "../../colors";
|
||||
|
||||
interface ShadeListProps {
|
||||
hex: string | null;
|
||||
hex: string;
|
||||
onChange: (color: string) => void;
|
||||
palette: ColorPaletteCustom;
|
||||
}
|
||||
|
||||
export const ShadeList = ({ hex, onChange, palette }: ShadeListProps) => {
|
||||
const colorObj = getColorNameAndShadeFromHex({
|
||||
hex: hex || "transparent",
|
||||
const colorObj = getColorNameAndShadeFromColor({
|
||||
color: hex || "transparent",
|
||||
palette,
|
||||
});
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
interface TopPicksProps {
|
||||
onChange: (color: string) => void;
|
||||
type: ColorPickerType;
|
||||
activeColor: string | null;
|
||||
activeColor: string;
|
||||
topPicks?: readonly string[];
|
||||
}
|
||||
|
||||
|
|
|
@ -6,23 +6,23 @@ import {
|
|||
MAX_CUSTOM_COLORS_USED_IN_CANVAS,
|
||||
} from "../../colors";
|
||||
|
||||
export const getColorNameAndShadeFromHex = ({
|
||||
export const getColorNameAndShadeFromColor = ({
|
||||
palette,
|
||||
hex,
|
||||
color,
|
||||
}: {
|
||||
palette: ColorPaletteCustom;
|
||||
hex: string;
|
||||
color: string;
|
||||
}): {
|
||||
colorName: ColorPickerColor;
|
||||
shade: number | null;
|
||||
} | null => {
|
||||
for (const [colorName, colorVal] of Object.entries(palette)) {
|
||||
if (Array.isArray(colorVal)) {
|
||||
const shade = colorVal.indexOf(hex);
|
||||
const shade = colorVal.indexOf(color);
|
||||
if (shade > -1) {
|
||||
return { colorName: colorName as ColorPickerColor, shade };
|
||||
}
|
||||
} else if (colorVal === hex) {
|
||||
} else if (colorVal === color) {
|
||||
return { colorName: colorName as ColorPickerColor, shade: null };
|
||||
}
|
||||
}
|
||||
|
@ -39,12 +39,9 @@ export const isCustomColor = ({
|
|||
color,
|
||||
palette,
|
||||
}: {
|
||||
color: string | null;
|
||||
color: string;
|
||||
palette: ColorPaletteCustom;
|
||||
}) => {
|
||||
if (!color) {
|
||||
return false;
|
||||
}
|
||||
const paletteValues = Object.values(palette).flat();
|
||||
return !paletteValues.includes(color);
|
||||
};
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { KEYS } from "../../keys";
|
||||
import {
|
||||
ColorPickerColor,
|
||||
ColorPalette,
|
||||
|
@ -5,12 +6,11 @@ import {
|
|||
COLORS_PER_ROW,
|
||||
COLOR_PALETTE,
|
||||
} from "../../colors";
|
||||
import { KEYS } from "../../keys";
|
||||
import { ValueOf } from "../../utility-types";
|
||||
import {
|
||||
ActiveColorPickerSectionAtomType,
|
||||
colorPickerHotkeyBindings,
|
||||
getColorNameAndShadeFromHex,
|
||||
getColorNameAndShadeFromColor,
|
||||
} from "./colorPickerUtils";
|
||||
|
||||
const arrowHandler = (
|
||||
|
@ -55,6 +55,9 @@ interface HotkeyHandlerProps {
|
|||
activeShade: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns true if the event was handled
|
||||
*/
|
||||
const hotkeyHandler = ({
|
||||
e,
|
||||
colorObj,
|
||||
|
@ -63,7 +66,7 @@ const hotkeyHandler = ({
|
|||
customColors,
|
||||
setActiveColorPickerSection,
|
||||
activeShade,
|
||||
}: HotkeyHandlerProps) => {
|
||||
}: HotkeyHandlerProps): boolean => {
|
||||
if (colorObj?.shade != null) {
|
||||
// shift + numpad is extremely messed up on windows apparently
|
||||
if (
|
||||
|
@ -73,6 +76,7 @@ const hotkeyHandler = ({
|
|||
const newShade = Number(e.code.slice(-1)) - 1;
|
||||
onChange(palette[colorObj.colorName][newShade]);
|
||||
setActiveColorPickerSection("shades");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -81,6 +85,7 @@ const hotkeyHandler = ({
|
|||
if (c) {
|
||||
onChange(customColors[Number(e.key) - 1]);
|
||||
setActiveColorPickerSection("custom");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -93,14 +98,16 @@ const hotkeyHandler = ({
|
|||
: paletteValue;
|
||||
onChange(r);
|
||||
setActiveColorPickerSection("baseColors");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
interface ColorPickerKeyNavHandlerProps {
|
||||
e: React.KeyboardEvent;
|
||||
event: React.KeyboardEvent;
|
||||
activeColorPickerSection: ActiveColorPickerSectionAtomType;
|
||||
palette: ColorPaletteCustom;
|
||||
hex: string | null;
|
||||
color: string;
|
||||
onChange: (color: string) => void;
|
||||
customColors: string[];
|
||||
setActiveColorPickerSection: (
|
||||
|
@ -108,27 +115,49 @@ interface ColorPickerKeyNavHandlerProps {
|
|||
) => void;
|
||||
updateData: (formData?: any) => void;
|
||||
activeShade: number;
|
||||
onEyeDropperToggle: (force?: boolean) => void;
|
||||
onEscape: (event: React.KeyboardEvent | KeyboardEvent) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns true if the event was handled
|
||||
*/
|
||||
export const colorPickerKeyNavHandler = ({
|
||||
e,
|
||||
event,
|
||||
activeColorPickerSection,
|
||||
palette,
|
||||
hex,
|
||||
color,
|
||||
onChange,
|
||||
customColors,
|
||||
setActiveColorPickerSection,
|
||||
updateData,
|
||||
activeShade,
|
||||
}: ColorPickerKeyNavHandlerProps) => {
|
||||
if (e.key === KEYS.ESCAPE || !hex) {
|
||||
updateData({ openPopup: null });
|
||||
return;
|
||||
onEyeDropperToggle,
|
||||
onEscape,
|
||||
}: ColorPickerKeyNavHandlerProps): boolean => {
|
||||
if (event[KEYS.CTRL_OR_CMD]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const colorObj = getColorNameAndShadeFromHex({ hex, palette });
|
||||
if (event.key === KEYS.ESCAPE) {
|
||||
onEscape(event);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (e.key === KEYS.TAB) {
|
||||
// checkt using `key` to ignore combos with Alt modifier
|
||||
if (event.key === KEYS.ALT) {
|
||||
onEyeDropperToggle(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.key === KEYS.I) {
|
||||
onEyeDropperToggle();
|
||||
return true;
|
||||
}
|
||||
|
||||
const colorObj = getColorNameAndShadeFromColor({ color, palette });
|
||||
|
||||
if (event.key === KEYS.TAB) {
|
||||
const sectionsMap: Record<
|
||||
NonNullable<ActiveColorPickerSectionAtomType>,
|
||||
boolean
|
||||
|
@ -147,7 +176,7 @@ export const colorPickerKeyNavHandler = ({
|
|||
}, [] as ActiveColorPickerSectionAtomType[]);
|
||||
|
||||
const activeSectionIndex = sections.indexOf(activeColorPickerSection);
|
||||
const indexOffset = e.shiftKey ? -1 : 1;
|
||||
const indexOffset = event.shiftKey ? -1 : 1;
|
||||
const nextSectionIndex =
|
||||
activeSectionIndex + indexOffset > sections.length - 1
|
||||
? 0
|
||||
|
@ -168,8 +197,8 @@ export const colorPickerKeyNavHandler = ({
|
|||
Object.entries(palette) as [string, ValueOf<ColorPalette>][]
|
||||
).find(([name, shades]) => {
|
||||
if (Array.isArray(shades)) {
|
||||
return shades.includes(hex);
|
||||
} else if (shades === hex) {
|
||||
return shades.includes(color);
|
||||
} else if (shades === color) {
|
||||
return name;
|
||||
}
|
||||
return null;
|
||||
|
@ -180,29 +209,34 @@ export const colorPickerKeyNavHandler = ({
|
|||
}
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
hotkeyHandler({
|
||||
e,
|
||||
colorObj,
|
||||
onChange,
|
||||
palette,
|
||||
customColors,
|
||||
setActiveColorPickerSection,
|
||||
activeShade,
|
||||
});
|
||||
if (
|
||||
hotkeyHandler({
|
||||
e: event,
|
||||
colorObj,
|
||||
onChange,
|
||||
palette,
|
||||
customColors,
|
||||
setActiveColorPickerSection,
|
||||
activeShade,
|
||||
})
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (activeColorPickerSection === "shades") {
|
||||
if (colorObj) {
|
||||
const { shade } = colorObj;
|
||||
const newShade = arrowHandler(e.key, shade, COLORS_PER_ROW);
|
||||
const newShade = arrowHandler(event.key, shade, COLORS_PER_ROW);
|
||||
|
||||
if (newShade !== undefined) {
|
||||
onChange(palette[colorObj.colorName][newShade]);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -214,7 +248,7 @@ export const colorPickerKeyNavHandler = ({
|
|||
const indexOfColorName = colorNames.indexOf(colorName);
|
||||
|
||||
const newColorIndex = arrowHandler(
|
||||
e.key,
|
||||
event.key,
|
||||
indexOfColorName,
|
||||
colorNames.length,
|
||||
);
|
||||
|
@ -228,15 +262,16 @@ export const colorPickerKeyNavHandler = ({
|
|||
? newColorNameValue[activeShade]
|
||||
: newColorNameValue,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (activeColorPickerSection === "custom") {
|
||||
const indexOfColor = customColors.indexOf(hex);
|
||||
const indexOfColor = customColors.indexOf(color);
|
||||
|
||||
const newColorIndex = arrowHandler(
|
||||
e.key,
|
||||
event.key,
|
||||
indexOfColor,
|
||||
customColors.length,
|
||||
);
|
||||
|
@ -244,6 +279,9 @@ export const colorPickerKeyNavHandler = ({
|
|||
if (newColorIndex !== undefined) {
|
||||
const newColor = customColors[newColorIndex];
|
||||
onChange(newColor);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
|
|
@ -31,7 +31,7 @@ const ConfirmDialog = (props: Props) => {
|
|||
return (
|
||||
<Dialog
|
||||
onCloseRequest={onCancel}
|
||||
small={true}
|
||||
size="small"
|
||||
{...rest}
|
||||
className={`confirm-dialog ${className}`}
|
||||
>
|
||||
|
|
|
@ -14,4 +14,33 @@
|
|||
padding: 0 0 0.75rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.Dialog__close {
|
||||
color: var(--color-gray-40);
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
top: 0.75rem;
|
||||
right: 0.5rem;
|
||||
border: 0;
|
||||
background-color: transparent;
|
||||
line-height: 0;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-gray-60);
|
||||
}
|
||||
&:active {
|
||||
color: var(--color-gray-40);
|
||||
}
|
||||
|
||||
@include isMobile {
|
||||
top: 1.25rem;
|
||||
right: 1.25rem;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,6 @@ import "./Dialog.scss";
|
|||
import { back, CloseIcon } from "./icons";
|
||||
import { Island } from "./Island";
|
||||
import { Modal } from "./Modal";
|
||||
import { AppState } from "../types";
|
||||
import { queryFocusableElements } from "../utils";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { isLibraryMenuOpenAtom } from "./LibraryMenu";
|
||||
|
@ -21,11 +20,10 @@ import { jotaiScope } from "../jotai";
|
|||
export interface DialogProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
small?: boolean;
|
||||
size?: "small" | "regular" | "wide";
|
||||
onCloseRequest(): void;
|
||||
title: React.ReactNode;
|
||||
title: React.ReactNode | false;
|
||||
autofocus?: boolean;
|
||||
theme?: AppState["theme"];
|
||||
closeOnClickOutside?: boolean;
|
||||
}
|
||||
|
||||
|
@ -33,6 +31,7 @@ export const Dialog = (props: DialogProps) => {
|
|||
const [islandNode, setIslandNode] = useCallbackRefState<HTMLDivElement>();
|
||||
const [lastActiveElement] = useState(document.activeElement);
|
||||
const { id } = useExcalidrawContainer();
|
||||
const device = useDevice();
|
||||
|
||||
useEffect(() => {
|
||||
if (!islandNode) {
|
||||
|
@ -86,23 +85,26 @@ export const Dialog = (props: DialogProps) => {
|
|||
<Modal
|
||||
className={clsx("Dialog", props.className)}
|
||||
labelledBy="dialog-title"
|
||||
maxWidth={props.small ? 550 : 800}
|
||||
maxWidth={
|
||||
props.size === "wide" ? 1024 : props.size === "small" ? 550 : 800
|
||||
}
|
||||
onCloseRequest={onClose}
|
||||
theme={props.theme}
|
||||
closeOnClickOutside={props.closeOnClickOutside}
|
||||
>
|
||||
<Island ref={setIslandNode}>
|
||||
<h2 id={`${id}-dialog-title`} className="Dialog__title">
|
||||
<span className="Dialog__titleContent">{props.title}</span>
|
||||
<button
|
||||
className="Modal__close"
|
||||
onClick={onClose}
|
||||
title={t("buttons.close")}
|
||||
aria-label={t("buttons.close")}
|
||||
>
|
||||
{useDevice().isMobile ? back : CloseIcon}
|
||||
</button>
|
||||
</h2>
|
||||
{props.title && (
|
||||
<h2 id={`${id}-dialog-title`} className="Dialog__title">
|
||||
<span className="Dialog__titleContent">{props.title}</span>
|
||||
</h2>
|
||||
)}
|
||||
<button
|
||||
className="Dialog__close"
|
||||
onClick={onClose}
|
||||
title={t("buttons.close")}
|
||||
aria-label={t("buttons.close")}
|
||||
>
|
||||
{device.isMobile ? back : CloseIcon}
|
||||
</button>
|
||||
<div className="Dialog__content">{props.children}</div>
|
||||
</Island>
|
||||
</Modal>
|
||||
|
|
|
@ -28,7 +28,7 @@ export const ErrorDialog = ({
|
|||
<>
|
||||
{modalIsShown && (
|
||||
<Dialog
|
||||
small
|
||||
size="small"
|
||||
onCloseRequest={handleClose}
|
||||
title={t("errorDialog.title")}
|
||||
>
|
||||
|
|
48
src/components/EyeDropper.scss
Normal file
48
src/components/EyeDropper.scss
Normal file
|
@ -0,0 +1,48 @@
|
|||
.excalidraw {
|
||||
.excalidraw-eye-dropper-container,
|
||||
.excalidraw-eye-dropper-backdrop {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 2;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.excalidraw-eye-dropper-container {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.excalidraw-eye-dropper-backdrop {
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.excalidraw-eye-dropper-preview {
|
||||
pointer-events: none;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
position: fixed;
|
||||
z-index: 999999;
|
||||
border-radius: 1rem;
|
||||
border: 1px solid var(--default-border-color);
|
||||
filter: var(--theme-filter);
|
||||
}
|
||||
|
||||
.excalidraw-eye-dropper-trigger {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
margin-right: -4px;
|
||||
margin-left: -2px;
|
||||
border-radius: 0.5rem;
|
||||
color: var(--icon-fill-color);
|
||||
|
||||
&:hover {
|
||||
background: var(--button-hover-bg);
|
||||
}
|
||||
&.selected {
|
||||
color: var(--color-primary);
|
||||
background: var(--color-primary-light);
|
||||
}
|
||||
}
|
||||
}
|
217
src/components/EyeDropper.tsx
Normal file
217
src/components/EyeDropper.tsx
Normal file
|
@ -0,0 +1,217 @@
|
|||
import { atom } from "jotai";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { COLOR_PALETTE, rgbToHex } from "../colors";
|
||||
import { EVENT } from "../constants";
|
||||
import { useUIAppState } from "../context/ui-appState";
|
||||
import { mutateElement } from "../element/mutateElement";
|
||||
import { useCreatePortalContainer } from "../hooks/useCreatePortalContainer";
|
||||
import { useOutsideClick } from "../hooks/useOutsideClick";
|
||||
import { KEYS } from "../keys";
|
||||
import { invalidateShapeForElement } from "../renderer/renderElement";
|
||||
import { getSelectedElements } from "../scene";
|
||||
import Scene from "../scene/Scene";
|
||||
import { useApp, useExcalidrawContainer, useExcalidrawElements } from "./App";
|
||||
|
||||
import "./EyeDropper.scss";
|
||||
|
||||
type EyeDropperProperties = {
|
||||
keepOpenOnAlt: boolean;
|
||||
swapPreviewOnAlt?: boolean;
|
||||
onSelect?: (color: string, event: PointerEvent) => void;
|
||||
previewType?: "strokeColor" | "backgroundColor";
|
||||
};
|
||||
|
||||
export const activeEyeDropperAtom = atom<null | EyeDropperProperties>(null);
|
||||
|
||||
export const EyeDropper: React.FC<{
|
||||
onCancel: () => void;
|
||||
onSelect: Required<EyeDropperProperties>["onSelect"];
|
||||
swapPreviewOnAlt?: EyeDropperProperties["swapPreviewOnAlt"];
|
||||
previewType?: EyeDropperProperties["previewType"];
|
||||
}> = ({
|
||||
onCancel,
|
||||
onSelect,
|
||||
swapPreviewOnAlt,
|
||||
previewType = "backgroundColor",
|
||||
}) => {
|
||||
const eyeDropperContainer = useCreatePortalContainer({
|
||||
className: "excalidraw-eye-dropper-backdrop",
|
||||
parentSelector: ".excalidraw-eye-dropper-container",
|
||||
});
|
||||
const appState = useUIAppState();
|
||||
const elements = useExcalidrawElements();
|
||||
const app = useApp();
|
||||
|
||||
const selectedElements = getSelectedElements(elements, appState);
|
||||
|
||||
const metaStuffRef = useRef({ selectedElements, app });
|
||||
metaStuffRef.current.selectedElements = selectedElements;
|
||||
metaStuffRef.current.app = app;
|
||||
|
||||
const { container: excalidrawContainer } = useExcalidrawContainer();
|
||||
|
||||
useEffect(() => {
|
||||
const colorPreviewDiv = ref.current;
|
||||
|
||||
if (!colorPreviewDiv || !app.canvas || !eyeDropperContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
let currentColor = COLOR_PALETTE.black;
|
||||
let isHoldingPointerDown = false;
|
||||
|
||||
const ctx = app.canvas.getContext("2d")!;
|
||||
|
||||
const mouseMoveListener = ({
|
||||
clientX,
|
||||
clientY,
|
||||
altKey,
|
||||
}: {
|
||||
clientX: number;
|
||||
clientY: number;
|
||||
altKey: boolean;
|
||||
}) => {
|
||||
// FIXME swap offset when the preview gets outside viewport
|
||||
colorPreviewDiv.style.top = `${clientY + 20}px`;
|
||||
colorPreviewDiv.style.left = `${clientX + 20}px`;
|
||||
|
||||
const pixel = ctx.getImageData(
|
||||
clientX * window.devicePixelRatio - appState.offsetLeft,
|
||||
clientY * window.devicePixelRatio - appState.offsetTop,
|
||||
1,
|
||||
1,
|
||||
).data;
|
||||
|
||||
currentColor = rgbToHex(pixel[0], pixel[1], pixel[2]);
|
||||
|
||||
if (isHoldingPointerDown) {
|
||||
for (const element of metaStuffRef.current.selectedElements) {
|
||||
mutateElement(
|
||||
element,
|
||||
{
|
||||
[altKey && swapPreviewOnAlt
|
||||
? previewType === "strokeColor"
|
||||
? "backgroundColor"
|
||||
: "strokeColor"
|
||||
: previewType]: currentColor,
|
||||
},
|
||||
false,
|
||||
);
|
||||
invalidateShapeForElement(element);
|
||||
}
|
||||
Scene.getScene(
|
||||
metaStuffRef.current.selectedElements[0],
|
||||
)?.informMutation();
|
||||
}
|
||||
|
||||
colorPreviewDiv.style.background = currentColor;
|
||||
};
|
||||
|
||||
const pointerDownListener = (event: PointerEvent) => {
|
||||
isHoldingPointerDown = true;
|
||||
// NOTE we can't event.preventDefault() as that would stop
|
||||
// pointermove events
|
||||
event.stopImmediatePropagation();
|
||||
};
|
||||
|
||||
const pointerUpListener = (event: PointerEvent) => {
|
||||
isHoldingPointerDown = false;
|
||||
|
||||
// since we're not preventing default on pointerdown, the focus would
|
||||
// goes back to `body` so we want to refocus the editor container instead
|
||||
excalidrawContainer?.focus();
|
||||
|
||||
event.stopImmediatePropagation();
|
||||
event.preventDefault();
|
||||
|
||||
onSelect(currentColor, event);
|
||||
};
|
||||
|
||||
const keyDownListener = (event: KeyboardEvent) => {
|
||||
if (event.key === KEYS.ESCAPE) {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
onCancel();
|
||||
}
|
||||
};
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
eyeDropperContainer.tabIndex = -1;
|
||||
// focus container so we can listen on keydown events
|
||||
eyeDropperContainer.focus();
|
||||
|
||||
// init color preview else it would show only after the first mouse move
|
||||
mouseMoveListener({
|
||||
clientX: metaStuffRef.current.app.lastViewportPosition.x,
|
||||
clientY: metaStuffRef.current.app.lastViewportPosition.y,
|
||||
altKey: false,
|
||||
});
|
||||
|
||||
eyeDropperContainer.addEventListener(EVENT.KEYDOWN, keyDownListener);
|
||||
eyeDropperContainer.addEventListener(
|
||||
EVENT.POINTER_DOWN,
|
||||
pointerDownListener,
|
||||
);
|
||||
eyeDropperContainer.addEventListener(EVENT.POINTER_UP, pointerUpListener);
|
||||
window.addEventListener("pointermove", mouseMoveListener, {
|
||||
passive: true,
|
||||
});
|
||||
window.addEventListener(EVENT.BLUR, onCancel);
|
||||
|
||||
return () => {
|
||||
isHoldingPointerDown = false;
|
||||
eyeDropperContainer.removeEventListener(EVENT.KEYDOWN, keyDownListener);
|
||||
eyeDropperContainer.removeEventListener(
|
||||
EVENT.POINTER_DOWN,
|
||||
pointerDownListener,
|
||||
);
|
||||
eyeDropperContainer.removeEventListener(
|
||||
EVENT.POINTER_UP,
|
||||
pointerUpListener,
|
||||
);
|
||||
window.removeEventListener("pointermove", mouseMoveListener);
|
||||
window.removeEventListener(EVENT.BLUR, onCancel);
|
||||
};
|
||||
}, [
|
||||
app.canvas,
|
||||
eyeDropperContainer,
|
||||
onCancel,
|
||||
onSelect,
|
||||
swapPreviewOnAlt,
|
||||
previewType,
|
||||
excalidrawContainer,
|
||||
appState.offsetLeft,
|
||||
appState.offsetTop,
|
||||
]);
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useOutsideClick(
|
||||
ref,
|
||||
() => {
|
||||
onCancel();
|
||||
},
|
||||
(event) => {
|
||||
if (
|
||||
event.target.closest(
|
||||
".excalidraw-eye-dropper-trigger, .excalidraw-eye-dropper-backdrop",
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
// consider all other clicks as outside
|
||||
return false;
|
||||
},
|
||||
);
|
||||
|
||||
if (!eyeDropperContainer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return createPortal(
|
||||
<div ref={ref} className="excalidraw-eye-dropper-preview" />,
|
||||
eyeDropperContainer,
|
||||
);
|
||||
};
|
95
src/components/FilledButton.scss
Normal file
95
src/components/FilledButton.scss
Normal file
|
@ -0,0 +1,95 @@
|
|||
@import "../css/variables.module";
|
||||
|
||||
.excalidraw {
|
||||
.ExcButton {
|
||||
&--color-primary {
|
||||
color: var(--input-bg-color);
|
||||
|
||||
--accent-color: var(--color-primary);
|
||||
--accent-color-hover: var(--color-primary-darker);
|
||||
--accent-color-active: var(--color-primary-darkest);
|
||||
}
|
||||
|
||||
&--color-danger {
|
||||
color: var(--input-bg-color);
|
||||
|
||||
--accent-color: var(--color-danger);
|
||||
--accent-color-hover: #d65550;
|
||||
--accent-color-active: #d1413c;
|
||||
}
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
flex-wrap: nowrap;
|
||||
|
||||
border-radius: 0.5rem;
|
||||
|
||||
font-family: "Assistant";
|
||||
|
||||
user-select: none;
|
||||
|
||||
transition: all 150ms ease-out;
|
||||
|
||||
&--size-large {
|
||||
font-weight: 400;
|
||||
font-size: 0.875rem;
|
||||
height: 3rem;
|
||||
padding: 0.5rem 1.5rem;
|
||||
gap: 0.75rem;
|
||||
|
||||
letter-spacing: 0.4px;
|
||||
}
|
||||
|
||||
&--size-medium {
|
||||
font-weight: 600;
|
||||
font-size: 0.75rem;
|
||||
height: 2.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
gap: 0.5rem;
|
||||
|
||||
letter-spacing: normal;
|
||||
}
|
||||
|
||||
&--variant-filled {
|
||||
background: var(--accent-color);
|
||||
border: 1px solid transparent;
|
||||
|
||||
&:hover {
|
||||
background: var(--accent-color-hover);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: var(--accent-color-active);
|
||||
}
|
||||
}
|
||||
|
||||
&--variant-outlined,
|
||||
&--variant-icon {
|
||||
border: 1px solid var(--accent-color);
|
||||
color: var(--accent-color);
|
||||
background: transparent;
|
||||
|
||||
&:hover {
|
||||
border: 1px solid var(--accent-color-hover);
|
||||
color: var(--accent-color-hover);
|
||||
}
|
||||
|
||||
&:active {
|
||||
border: 1px solid var(--accent-color-active);
|
||||
color: var(--accent-color-active);
|
||||
}
|
||||
}
|
||||
|
||||
&--variant-icon {
|
||||
padding: 0.5rem 0.75rem;
|
||||
width: 3rem;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
}
|
||||
}
|
61
src/components/FilledButton.tsx
Normal file
61
src/components/FilledButton.tsx
Normal file
|
@ -0,0 +1,61 @@
|
|||
import React, { forwardRef } from "react";
|
||||
import clsx from "clsx";
|
||||
|
||||
import "./FilledButton.scss";
|
||||
|
||||
export type ButtonVariant = "filled" | "outlined" | "icon";
|
||||
export type ButtonColor = "primary" | "danger";
|
||||
export type ButtonSize = "medium" | "large";
|
||||
|
||||
export type FilledButtonProps = {
|
||||
label: string;
|
||||
|
||||
children?: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
|
||||
variant?: ButtonVariant;
|
||||
color?: ButtonColor;
|
||||
size?: ButtonSize;
|
||||
className?: string;
|
||||
|
||||
startIcon?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const FilledButton = forwardRef<HTMLButtonElement, FilledButtonProps>(
|
||||
(
|
||||
{
|
||||
children,
|
||||
startIcon,
|
||||
onClick,
|
||||
label,
|
||||
variant = "filled",
|
||||
color = "primary",
|
||||
size = "medium",
|
||||
className,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
return (
|
||||
<button
|
||||
className={clsx(
|
||||
"ExcButton",
|
||||
`ExcButton--color-${color}`,
|
||||
`ExcButton--variant-${variant}`,
|
||||
`ExcButton--size-${size}`,
|
||||
className,
|
||||
)}
|
||||
onClick={onClick}
|
||||
type="button"
|
||||
aria-label={label}
|
||||
ref={ref}
|
||||
>
|
||||
{startIcon && (
|
||||
<div className="ExcButton__icon" aria-hidden>
|
||||
{startIcon}
|
||||
</div>
|
||||
)}
|
||||
{variant !== "icon" && (children ?? label)}
|
||||
</button>
|
||||
);
|
||||
},
|
||||
);
|
|
@ -164,6 +164,10 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
|||
label={t("toolBar.eraser")}
|
||||
shortcuts={[KEYS.E, KEYS["0"]]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.eyeDropper")}
|
||||
shortcuts={[KEYS.I, "Shift+S", "Shift+G"]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("helpDialog.editLineArrowPoints")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+Enter")]}
|
||||
|
|
173
src/components/ImageExportDialog.scss
Normal file
173
src/components/ImageExportDialog.scss
Normal file
|
@ -0,0 +1,173 @@
|
|||
@import "../css/variables.module";
|
||||
|
||||
.excalidraw {
|
||||
--ImageExportModal-preview-border: #d6d6d6;
|
||||
|
||||
&.theme--dark {
|
||||
--ImageExportModal-preview-border: #5c5c5c;
|
||||
}
|
||||
|
||||
.ImageExportModal {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
|
||||
& h3 {
|
||||
font-family: "Assistant";
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-size: 1.313rem;
|
||||
line-height: 130%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
@include isMobile {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
& > h3 {
|
||||
display: none;
|
||||
|
||||
@include isMobile {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@include isMobile {
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 5rem);
|
||||
}
|
||||
|
||||
&__preview {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: 360px;
|
||||
width: 55%;
|
||||
|
||||
margin-right: 1.5rem;
|
||||
|
||||
@include isMobile {
|
||||
max-width: unset;
|
||||
margin-right: unset;
|
||||
|
||||
width: 100%;
|
||||
height: unset;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
&__filename {
|
||||
& > input {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
&__canvas {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAMUlEQVQ4T2NkYGAQYcAP3uCTZhw1gGGYhAGBZIA/nYDCgBDAm9BGDWAAJyRCgLaBCAAgXwixzAS0pgAAAABJRU5ErkJggg==")
|
||||
left center;
|
||||
|
||||
border: 1px solid var(--ImageExportModal-preview-border);
|
||||
border-radius: 12px;
|
||||
|
||||
overflow: hidden;
|
||||
padding: 1rem;
|
||||
|
||||
& > canvas {
|
||||
max-width: calc(100% - 2rem);
|
||||
max-height: calc(100% - 2rem);
|
||||
|
||||
filter: none !important;
|
||||
|
||||
@include isMobile {
|
||||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@include isMobile {
|
||||
margin-top: 24px;
|
||||
max-width: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__settings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
gap: 18px;
|
||||
|
||||
@include isMobile {
|
||||
margin-left: unset;
|
||||
margin-top: 1rem;
|
||||
flex-direction: row;
|
||||
gap: 6px 34px;
|
||||
|
||||
align-content: flex-start;
|
||||
}
|
||||
|
||||
&__setting {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
@include isMobile {
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
justify-content: unset;
|
||||
height: 52px;
|
||||
}
|
||||
|
||||
&__label {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
font-family: "Assistant";
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
line-height: 150%;
|
||||
|
||||
& svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
&__buttons {
|
||||
flex-grow: 1;
|
||||
flex-wrap: wrap;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 11px;
|
||||
|
||||
align-items: flex-end;
|
||||
align-content: flex-end;
|
||||
|
||||
@include isMobile {
|
||||
padding-top: 32px;
|
||||
flex-basis: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,25 +1,39 @@
|
|||
import React, { useEffect, useRef, useState } from "react";
|
||||
|
||||
import type { ActionManager } from "../actions/manager";
|
||||
import type { AppClassProperties, BinaryFiles, UIAppState } from "../types";
|
||||
|
||||
import {
|
||||
actionExportWithDarkMode,
|
||||
actionChangeExportBackground,
|
||||
actionChangeExportEmbedScene,
|
||||
actionChangeExportScale,
|
||||
actionChangeProjectName,
|
||||
} from "../actions/actionExport";
|
||||
import { probablySupportsClipboardBlob } from "../clipboard";
|
||||
import { canvasToBlob } from "../data/blob";
|
||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||
import { t } from "../i18n";
|
||||
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
||||
import { AppClassProperties, BinaryFiles, UIAppState } from "../types";
|
||||
import { Dialog } from "./Dialog";
|
||||
import { clipboard } from "./icons";
|
||||
import Stack from "./Stack";
|
||||
import OpenColor from "open-color";
|
||||
import { CheckboxItem } from "./CheckboxItem";
|
||||
import {
|
||||
DEFAULT_EXPORT_PADDING,
|
||||
EXPORT_IMAGE_TYPES,
|
||||
isFirefox,
|
||||
EXPORT_SCALES,
|
||||
} from "../constants";
|
||||
|
||||
import { canvasToBlob } from "../data/blob";
|
||||
import { nativeFileSystemSupported } from "../data/filesystem";
|
||||
import { ActionManager } from "../actions/manager";
|
||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||
import { t } from "../i18n";
|
||||
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
||||
import { exportToCanvas } from "../packages/utils";
|
||||
|
||||
import "./ExportDialog.scss";
|
||||
import { copyIcon, downloadIcon, helpIcon } from "./icons";
|
||||
import { Dialog } from "./Dialog";
|
||||
import { RadioGroup } from "./RadioGroup";
|
||||
import { Switch } from "./Switch";
|
||||
import { Tooltip } from "./Tooltip";
|
||||
|
||||
import "./ImageExportDialog.scss";
|
||||
import { useAppProps } from "./App";
|
||||
import { FilledButton } from "./FilledButton";
|
||||
|
||||
const supportsContextFilters =
|
||||
"filter" in document.createElement("canvas").getContext("2d")!;
|
||||
|
@ -36,50 +50,36 @@ export const ErrorCanvasPreview = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export type ExportCB = (
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
scale?: number,
|
||||
) => void;
|
||||
|
||||
const ExportButton: React.FC<{
|
||||
color: keyof OpenColor;
|
||||
onClick: () => void;
|
||||
title: string;
|
||||
shade?: number;
|
||||
children?: React.ReactNode;
|
||||
}> = ({ children, title, onClick, color, shade = 6 }) => {
|
||||
return (
|
||||
<button
|
||||
className="ExportDialog-imageExportButton"
|
||||
style={{
|
||||
["--button-color" as any]: OpenColor[color][shade],
|
||||
["--button-color-darker" as any]: OpenColor[color][shade + 1],
|
||||
["--button-color-darkest" as any]: OpenColor[color][shade + 2],
|
||||
}}
|
||||
title={title}
|
||||
aria-label={title}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const ImageExportModal = ({
|
||||
elements,
|
||||
appState,
|
||||
files,
|
||||
actionManager,
|
||||
onExportImage,
|
||||
}: {
|
||||
type ImageExportModalProps = {
|
||||
appState: UIAppState;
|
||||
elements: readonly NonDeletedExcalidrawElement[];
|
||||
files: BinaryFiles;
|
||||
actionManager: ActionManager;
|
||||
onExportImage: AppClassProperties["onExportImage"];
|
||||
}) => {
|
||||
};
|
||||
|
||||
const ImageExportModal = ({
|
||||
appState,
|
||||
elements,
|
||||
files,
|
||||
actionManager,
|
||||
onExportImage,
|
||||
}: ImageExportModalProps) => {
|
||||
const appProps = useAppProps();
|
||||
const [projectName, setProjectName] = useState(appState.name);
|
||||
|
||||
const someElementIsSelected = isSomeElementSelected(elements, appState);
|
||||
|
||||
const [exportSelected, setExportSelected] = useState(someElementIsSelected);
|
||||
const [exportWithBackground, setExportWithBackground] = useState(
|
||||
appState.exportBackground,
|
||||
);
|
||||
const [exportDarkMode, setExportDarkMode] = useState(
|
||||
appState.exportWithDarkMode,
|
||||
);
|
||||
const [embedScene, setEmbedScene] = useState(appState.exportEmbedScene);
|
||||
const [exportScale, setExportScale] = useState(appState.exportScale);
|
||||
|
||||
const previewRef = useRef<HTMLDivElement>(null);
|
||||
const [renderError, setRenderError] = useState<Error | null>(null);
|
||||
|
||||
|
@ -93,6 +93,7 @@ const ImageExportModal = ({
|
|||
return;
|
||||
}
|
||||
const maxWidth = previewNode.offsetWidth;
|
||||
const maxHeight = previewNode.offsetHeight;
|
||||
if (!maxWidth) {
|
||||
return;
|
||||
}
|
||||
|
@ -101,7 +102,7 @@ const ImageExportModal = ({
|
|||
appState,
|
||||
files,
|
||||
exportPadding: DEFAULT_EXPORT_PADDING,
|
||||
maxWidthOrHeight: maxWidth,
|
||||
maxWidthOrHeight: Math.max(maxWidth, maxHeight),
|
||||
})
|
||||
.then((canvas) => {
|
||||
setRenderError(null);
|
||||
|
@ -118,89 +119,190 @@ const ImageExportModal = ({
|
|||
}, [appState, files, exportedElements]);
|
||||
|
||||
return (
|
||||
<div className="ExportDialog">
|
||||
<div className="ExportDialog__preview" ref={previewRef}>
|
||||
{renderError && <ErrorCanvasPreview />}
|
||||
</div>
|
||||
{supportsContextFilters &&
|
||||
actionManager.renderAction("exportWithDarkMode")}
|
||||
<div style={{ display: "grid", gridTemplateColumns: "1fr" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(auto-fit, minmax(190px, 1fr))",
|
||||
// dunno why this is needed, but when the items wrap it creates
|
||||
// an overflow
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{actionManager.renderAction("changeExportBackground")}
|
||||
{someElementIsSelected && (
|
||||
<CheckboxItem
|
||||
checked={exportSelected}
|
||||
onChange={(checked) => setExportSelected(checked)}
|
||||
>
|
||||
{t("labels.onlySelected")}
|
||||
</CheckboxItem>
|
||||
<div className="ImageExportModal">
|
||||
<h3>{t("imageExportDialog.header")}</h3>
|
||||
<div className="ImageExportModal__preview">
|
||||
<div className="ImageExportModal__preview__canvas" ref={previewRef}>
|
||||
{renderError && <ErrorCanvasPreview />}
|
||||
</div>
|
||||
<div className="ImageExportModal__preview__filename">
|
||||
{!nativeFileSystemSupported && (
|
||||
<input
|
||||
type="text"
|
||||
className="TextInput"
|
||||
value={projectName}
|
||||
style={{ width: "30ch" }}
|
||||
disabled={
|
||||
typeof appProps.name !== "undefined" || appState.viewModeEnabled
|
||||
}
|
||||
onChange={(event) => {
|
||||
setProjectName(event.target.value);
|
||||
actionManager.executeAction(
|
||||
actionChangeProjectName,
|
||||
"ui",
|
||||
event.target.value,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{actionManager.renderAction("changeExportEmbedScene")}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: "flex", alignItems: "center", marginTop: ".6em" }}>
|
||||
<Stack.Row gap={2}>
|
||||
{actionManager.renderAction("changeExportScale")}
|
||||
</Stack.Row>
|
||||
<p style={{ marginLeft: "1em", userSelect: "none" }}>
|
||||
{t("buttons.scale")}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
margin: ".6em 0",
|
||||
}}
|
||||
>
|
||||
{!nativeFileSystemSupported &&
|
||||
actionManager.renderAction("changeProjectName")}
|
||||
</div>
|
||||
<Stack.Row gap={2} justifyContent="center" style={{ margin: "2em 0" }}>
|
||||
<ExportButton
|
||||
color="indigo"
|
||||
title={t("buttons.exportToPng")}
|
||||
aria-label={t("buttons.exportToPng")}
|
||||
onClick={() =>
|
||||
onExportImage(EXPORT_IMAGE_TYPES.png, exportedElements)
|
||||
}
|
||||
>
|
||||
PNG
|
||||
</ExportButton>
|
||||
<ExportButton
|
||||
color="red"
|
||||
title={t("buttons.exportToSvg")}
|
||||
aria-label={t("buttons.exportToSvg")}
|
||||
onClick={() =>
|
||||
onExportImage(EXPORT_IMAGE_TYPES.svg, exportedElements)
|
||||
}
|
||||
>
|
||||
SVG
|
||||
</ExportButton>
|
||||
{/* firefox supports clipboard API under a flag,
|
||||
so let's throw and tell people what they can do */}
|
||||
{(probablySupportsClipboardBlob || isFirefox) && (
|
||||
<ExportButton
|
||||
title={t("buttons.copyPngToClipboard")}
|
||||
onClick={() =>
|
||||
onExportImage(EXPORT_IMAGE_TYPES.clipboard, exportedElements)
|
||||
}
|
||||
color="gray"
|
||||
shade={7}
|
||||
<div className="ImageExportModal__settings">
|
||||
<h3>{t("imageExportDialog.header")}</h3>
|
||||
{someElementIsSelected && (
|
||||
<ExportSetting
|
||||
label={t("imageExportDialog.label.onlySelected")}
|
||||
name="exportOnlySelected"
|
||||
>
|
||||
{clipboard}
|
||||
</ExportButton>
|
||||
<Switch
|
||||
name="exportOnlySelected"
|
||||
checked={exportSelected}
|
||||
onChange={(checked) => {
|
||||
setExportSelected(checked);
|
||||
}}
|
||||
/>
|
||||
</ExportSetting>
|
||||
)}
|
||||
</Stack.Row>
|
||||
<ExportSetting
|
||||
label={t("imageExportDialog.label.withBackground")}
|
||||
name="exportBackgroundSwitch"
|
||||
>
|
||||
<Switch
|
||||
name="exportBackgroundSwitch"
|
||||
checked={exportWithBackground}
|
||||
onChange={(checked) => {
|
||||
setExportWithBackground(checked);
|
||||
actionManager.executeAction(
|
||||
actionChangeExportBackground,
|
||||
"ui",
|
||||
checked,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</ExportSetting>
|
||||
{supportsContextFilters && (
|
||||
<ExportSetting
|
||||
label={t("imageExportDialog.label.darkMode")}
|
||||
name="exportDarkModeSwitch"
|
||||
>
|
||||
<Switch
|
||||
name="exportDarkModeSwitch"
|
||||
checked={exportDarkMode}
|
||||
onChange={(checked) => {
|
||||
setExportDarkMode(checked);
|
||||
actionManager.executeAction(
|
||||
actionExportWithDarkMode,
|
||||
"ui",
|
||||
checked,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</ExportSetting>
|
||||
)}
|
||||
<ExportSetting
|
||||
label={t("imageExportDialog.label.embedScene")}
|
||||
tooltip={t("imageExportDialog.tooltip.embedScene")}
|
||||
name="exportEmbedSwitch"
|
||||
>
|
||||
<Switch
|
||||
name="exportEmbedSwitch"
|
||||
checked={embedScene}
|
||||
onChange={(checked) => {
|
||||
setEmbedScene(checked);
|
||||
actionManager.executeAction(
|
||||
actionChangeExportEmbedScene,
|
||||
"ui",
|
||||
checked,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</ExportSetting>
|
||||
<ExportSetting
|
||||
label={t("imageExportDialog.label.scale")}
|
||||
name="exportScale"
|
||||
>
|
||||
<RadioGroup
|
||||
name="exportScale"
|
||||
value={exportScale}
|
||||
onChange={(scale) => {
|
||||
setExportScale(scale);
|
||||
actionManager.executeAction(actionChangeExportScale, "ui", scale);
|
||||
}}
|
||||
choices={EXPORT_SCALES.map((scale) => ({
|
||||
value: scale,
|
||||
label: `${scale}\u00d7`,
|
||||
}))}
|
||||
/>
|
||||
</ExportSetting>
|
||||
|
||||
<div className="ImageExportModal__settings__buttons">
|
||||
<FilledButton
|
||||
className="ImageExportModal__settings__buttons__button"
|
||||
label={t("imageExportDialog.title.exportToPng")}
|
||||
onClick={() =>
|
||||
onExportImage(EXPORT_IMAGE_TYPES.png, exportedElements)
|
||||
}
|
||||
startIcon={downloadIcon}
|
||||
>
|
||||
{t("imageExportDialog.button.exportToPng")}
|
||||
</FilledButton>
|
||||
<FilledButton
|
||||
className="ImageExportModal__settings__buttons__button"
|
||||
label={t("imageExportDialog.title.exportToSvg")}
|
||||
onClick={() =>
|
||||
onExportImage(EXPORT_IMAGE_TYPES.svg, exportedElements)
|
||||
}
|
||||
startIcon={downloadIcon}
|
||||
>
|
||||
{t("imageExportDialog.button.exportToSvg")}
|
||||
</FilledButton>
|
||||
{(probablySupportsClipboardBlob || isFirefox) && (
|
||||
<FilledButton
|
||||
className="ImageExportModal__settings__buttons__button"
|
||||
label={t("imageExportDialog.title.copyPngToClipboard")}
|
||||
onClick={() =>
|
||||
onExportImage(EXPORT_IMAGE_TYPES.clipboard, exportedElements)
|
||||
}
|
||||
startIcon={copyIcon}
|
||||
>
|
||||
{t("imageExportDialog.button.copyPngToClipboard")}
|
||||
</FilledButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type ExportSettingProps = {
|
||||
label: string;
|
||||
children: React.ReactNode;
|
||||
tooltip?: string;
|
||||
name?: string;
|
||||
};
|
||||
|
||||
const ExportSetting = ({
|
||||
label,
|
||||
children,
|
||||
tooltip,
|
||||
name,
|
||||
}: ExportSettingProps) => {
|
||||
return (
|
||||
<div className="ImageExportModal__settings__setting" title={label}>
|
||||
<label
|
||||
htmlFor={name}
|
||||
className="ImageExportModal__settings__setting__label"
|
||||
>
|
||||
{label}
|
||||
{tooltip && (
|
||||
<Tooltip label={tooltip} long={true}>
|
||||
{helpIcon}
|
||||
</Tooltip>
|
||||
)}
|
||||
</label>
|
||||
<div className="ImageExportModal__settings__setting__content">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -225,7 +327,7 @@ export const ImageExportDialog = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<Dialog onCloseRequest={onCloseRequest} title={t("buttons.exportImage")}>
|
||||
<Dialog onCloseRequest={onCloseRequest} size="wide" title={false}>
|
||||
<ImageExportModal
|
||||
elements={elements}
|
||||
appState={appState}
|
||||
|
|
|
@ -38,7 +38,7 @@ import { actionToggleStats } from "../actions/actionToggleStats";
|
|||
import Footer from "./footer/Footer";
|
||||
import { isSidebarDockedAtom } from "./Sidebar/Sidebar";
|
||||
import { jotaiScope } from "../jotai";
|
||||
import { Provider, useAtomValue } from "jotai";
|
||||
import { Provider, useAtom, useAtomValue } from "jotai";
|
||||
import MainMenu from "./main-menu/MainMenu";
|
||||
import { ActiveConfirmDialog } from "./ActiveConfirmDialog";
|
||||
import { HandButton } from "./HandButton";
|
||||
|
@ -47,6 +47,7 @@ import { TunnelsContext, useInitializeTunnels } from "../context/tunnels";
|
|||
import { LibraryIcon } from "./icons";
|
||||
import { UIAppStateContext } from "../context/ui-appState";
|
||||
import { DefaultSidebar } from "./DefaultSidebar";
|
||||
import { EyeDropper, activeEyeDropperAtom } from "./EyeDropper";
|
||||
|
||||
import "./LayerUI.scss";
|
||||
import "./Toolbar.scss";
|
||||
|
@ -120,6 +121,11 @@ const LayerUI = ({
|
|||
const device = useDevice();
|
||||
const tunnels = useInitializeTunnels();
|
||||
|
||||
const [eyeDropperState, setEyeDropperState] = useAtom(
|
||||
activeEyeDropperAtom,
|
||||
jotaiScope,
|
||||
);
|
||||
|
||||
const renderJSONExportDialog = () => {
|
||||
if (!UIOptions.canvasActions.export) {
|
||||
return null;
|
||||
|
@ -350,6 +356,21 @@ const LayerUI = ({
|
|||
{appState.errorMessage}
|
||||
</ErrorDialog>
|
||||
)}
|
||||
{eyeDropperState && !device.isMobile && (
|
||||
<EyeDropper
|
||||
swapPreviewOnAlt={eyeDropperState.swapPreviewOnAlt}
|
||||
previewType={eyeDropperState.previewType}
|
||||
onCancel={() => {
|
||||
setEyeDropperState(null);
|
||||
}}
|
||||
onSelect={(color, event) => {
|
||||
setEyeDropperState((state) => {
|
||||
return state?.keepOpenOnAlt && event.altKey ? state : null;
|
||||
});
|
||||
eyeDropperState?.onSelect?.(color, event);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{appState.openDialog === "help" && (
|
||||
<HelpDialog
|
||||
onClose={() => {
|
||||
|
@ -371,7 +392,7 @@ const LayerUI = ({
|
|||
}
|
||||
/>
|
||||
)}
|
||||
{device.isMobile && (
|
||||
{device.isMobile && !eyeDropperState && (
|
||||
<MobileMenu
|
||||
appState={appState}
|
||||
elements={elements}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState, useCallback } from "react";
|
||||
import React, { useState, useCallback, useMemo, useRef } from "react";
|
||||
import Library, {
|
||||
distributeLibraryItemsOnSquareGrid,
|
||||
libraryItemsAtom,
|
||||
|
@ -27,6 +27,8 @@ import { useUIAppState } from "../context/ui-appState";
|
|||
|
||||
import "./LibraryMenu.scss";
|
||||
import { LibraryMenuControlButtons } from "./LibraryMenuControlButtons";
|
||||
import { isShallowEqual } from "../utils";
|
||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||
|
||||
export const isLibraryMenuOpenAtom = atom(false);
|
||||
|
||||
|
@ -42,7 +44,7 @@ export const LibraryMenuContent = ({
|
|||
libraryReturnUrl,
|
||||
library,
|
||||
id,
|
||||
appState,
|
||||
theme,
|
||||
selectedItems,
|
||||
onSelectItems,
|
||||
}: {
|
||||
|
@ -53,35 +55,47 @@ export const LibraryMenuContent = ({
|
|||
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
|
||||
library: Library;
|
||||
id: string;
|
||||
appState: UIAppState;
|
||||
theme: UIAppState["theme"];
|
||||
selectedItems: LibraryItem["id"][];
|
||||
onSelectItems: (id: LibraryItem["id"][]) => void;
|
||||
}) => {
|
||||
const [libraryItemsData] = useAtom(libraryItemsAtom, jotaiScope);
|
||||
|
||||
const addToLibrary = useCallback(
|
||||
async (elements: LibraryItem["elements"], libraryItems: LibraryItems) => {
|
||||
trackEvent("element", "addToLibrary", "ui");
|
||||
if (elements.some((element) => element.type === "image")) {
|
||||
return setAppState({
|
||||
errorMessage: "Support for adding images to the library coming soon!",
|
||||
const _onAddToLibrary = useCallback(
|
||||
(elements: LibraryItem["elements"]) => {
|
||||
const addToLibrary = async (
|
||||
processedElements: LibraryItem["elements"],
|
||||
libraryItems: LibraryItems,
|
||||
) => {
|
||||
trackEvent("element", "addToLibrary", "ui");
|
||||
if (processedElements.some((element) => element.type === "image")) {
|
||||
return setAppState({
|
||||
errorMessage:
|
||||
"Support for adding images to the library coming soon!",
|
||||
});
|
||||
}
|
||||
const nextItems: LibraryItems = [
|
||||
{
|
||||
status: "unpublished",
|
||||
elements: processedElements,
|
||||
id: randomId(),
|
||||
created: Date.now(),
|
||||
},
|
||||
...libraryItems,
|
||||
];
|
||||
onAddToLibrary();
|
||||
library.setLibrary(nextItems).catch(() => {
|
||||
setAppState({ errorMessage: t("alerts.errorAddingToLibrary") });
|
||||
});
|
||||
}
|
||||
const nextItems: LibraryItems = [
|
||||
{
|
||||
status: "unpublished",
|
||||
elements,
|
||||
id: randomId(),
|
||||
created: Date.now(),
|
||||
},
|
||||
...libraryItems,
|
||||
];
|
||||
onAddToLibrary();
|
||||
library.setLibrary(nextItems).catch(() => {
|
||||
setAppState({ errorMessage: t("alerts.errorAddingToLibrary") });
|
||||
});
|
||||
};
|
||||
addToLibrary(elements, libraryItemsData.libraryItems);
|
||||
},
|
||||
[onAddToLibrary, library, setAppState],
|
||||
[onAddToLibrary, library, setAppState, libraryItemsData.libraryItems],
|
||||
);
|
||||
|
||||
const libraryItems = useMemo(
|
||||
() => libraryItemsData.libraryItems,
|
||||
[libraryItemsData],
|
||||
);
|
||||
|
||||
if (
|
||||
|
@ -107,17 +121,15 @@ export const LibraryMenuContent = ({
|
|||
<LibraryMenuWrapper>
|
||||
<LibraryMenuItems
|
||||
isLoading={libraryItemsData.status === "loading"}
|
||||
libraryItems={libraryItemsData.libraryItems}
|
||||
onAddToLibrary={(elements) =>
|
||||
addToLibrary(elements, libraryItemsData.libraryItems)
|
||||
}
|
||||
libraryItems={libraryItems}
|
||||
onAddToLibrary={_onAddToLibrary}
|
||||
onInsertLibraryItems={onInsertLibraryItems}
|
||||
pendingElements={pendingElements}
|
||||
selectedItems={selectedItems}
|
||||
onSelectItems={onSelectItems}
|
||||
id={id}
|
||||
libraryReturnUrl={libraryReturnUrl}
|
||||
theme={appState.theme}
|
||||
theme={theme}
|
||||
onSelectItems={onSelectItems}
|
||||
selectedItems={selectedItems}
|
||||
/>
|
||||
{showBtn && (
|
||||
<LibraryMenuControlButtons
|
||||
|
@ -125,13 +137,36 @@ export const LibraryMenuContent = ({
|
|||
style={{ padding: "16px 12px 0 12px" }}
|
||||
id={id}
|
||||
libraryReturnUrl={libraryReturnUrl}
|
||||
theme={appState.theme}
|
||||
theme={theme}
|
||||
/>
|
||||
)}
|
||||
</LibraryMenuWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const usePendingElementsMemo = (
|
||||
appState: UIAppState,
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
) => {
|
||||
const create = () => getSelectedElements(elements, appState, true);
|
||||
const val = useRef(create());
|
||||
const prevAppState = useRef<UIAppState>(appState);
|
||||
const prevElements = useRef(elements);
|
||||
|
||||
if (
|
||||
!isShallowEqual(
|
||||
appState.selectedElementIds,
|
||||
prevAppState.current.selectedElementIds,
|
||||
) ||
|
||||
!isShallowEqual(elements, prevElements.current)
|
||||
) {
|
||||
val.current = create();
|
||||
prevAppState.current = appState;
|
||||
prevElements.current = elements;
|
||||
}
|
||||
return val.current;
|
||||
};
|
||||
|
||||
/**
|
||||
* This component is meant to be rendered inside <Sidebar.Tab/> inside our
|
||||
* <DefaultSidebar/> or host apps Sidebar components.
|
||||
|
@ -142,8 +177,17 @@ export const LibraryMenu = () => {
|
|||
const appState = useUIAppState();
|
||||
const setAppState = useExcalidrawSetAppState();
|
||||
const elements = useExcalidrawElements();
|
||||
|
||||
const [selectedItems, setSelectedItems] = useState<LibraryItem["id"][]>([]);
|
||||
const memoizedLibrary = useMemo(() => library, [library]);
|
||||
// BUG: pendingElements are still causing some unnecessary rerenders because clicking into canvas returns some ids even when no element is selected.
|
||||
const pendingElements = usePendingElementsMemo(appState, elements);
|
||||
|
||||
const onInsertLibraryItems = useCallback(
|
||||
(libraryItems: LibraryItems) => {
|
||||
onInsertElements(distributeLibraryItemsOnSquareGrid(libraryItems));
|
||||
},
|
||||
[onInsertElements],
|
||||
);
|
||||
|
||||
const deselectItems = useCallback(() => {
|
||||
setAppState({
|
||||
|
@ -154,16 +198,14 @@ export const LibraryMenu = () => {
|
|||
|
||||
return (
|
||||
<LibraryMenuContent
|
||||
pendingElements={getSelectedElements(elements, appState, true)}
|
||||
onInsertLibraryItems={(libraryItems) => {
|
||||
onInsertElements(distributeLibraryItemsOnSquareGrid(libraryItems));
|
||||
}}
|
||||
pendingElements={pendingElements}
|
||||
onInsertLibraryItems={onInsertLibraryItems}
|
||||
onAddToLibrary={deselectItems}
|
||||
setAppState={setAppState}
|
||||
libraryReturnUrl={appProps.libraryReturnUrl}
|
||||
library={library}
|
||||
library={memoizedLibrary}
|
||||
id={id}
|
||||
appState={appState}
|
||||
theme={appState.theme}
|
||||
selectedItems={selectedItems}
|
||||
onSelectItems={setSelectedItems}
|
||||
/>
|
||||
|
|
|
@ -24,6 +24,7 @@ import DropdownMenu from "./dropdownMenu/DropdownMenu";
|
|||
import { isLibraryMenuOpenAtom } from "./LibraryMenu";
|
||||
import { useUIAppState } from "../context/ui-appState";
|
||||
import clsx from "clsx";
|
||||
import { useLibraryCache } from "../hooks/useLibraryItemSvg";
|
||||
|
||||
const getSelectedItems = (
|
||||
libraryItems: LibraryItems,
|
||||
|
@ -55,7 +56,7 @@ export const LibraryDropdownMenuButton: React.FC<{
|
|||
jotaiScope,
|
||||
);
|
||||
|
||||
const renderRemoveLibAlert = useCallback(() => {
|
||||
const renderRemoveLibAlert = () => {
|
||||
const content = selectedItems.length
|
||||
? t("alerts.removeItemsFromsLibrary", { count: selectedItems.length })
|
||||
: t("alerts.resetLibrary");
|
||||
|
@ -80,7 +81,7 @@ export const LibraryDropdownMenuButton: React.FC<{
|
|||
<p>{content}</p>
|
||||
</ConfirmDialog>
|
||||
);
|
||||
}, [selectedItems, onRemoveFromLibrary, resetLibrary]);
|
||||
};
|
||||
|
||||
const [showRemoveLibAlert, setShowRemoveLibAlert] = useState(false);
|
||||
|
||||
|
@ -106,7 +107,7 @@ export const LibraryDropdownMenuButton: React.FC<{
|
|||
onCloseRequest={() => setPublishLibSuccess(null)}
|
||||
title={t("publishSuccessDialog.title")}
|
||||
className="publish-library-success"
|
||||
small={true}
|
||||
size="small"
|
||||
>
|
||||
<p>
|
||||
<Trans
|
||||
|
@ -136,20 +137,20 @@ export const LibraryDropdownMenuButton: React.FC<{
|
|||
);
|
||||
}, [setPublishLibSuccess, publishLibSuccess]);
|
||||
|
||||
const onPublishLibSuccess = useCallback(
|
||||
(data: { url: string; authorName: string }, libraryItems: LibraryItems) => {
|
||||
setShowPublishLibraryDialog(false);
|
||||
setPublishLibSuccess({ url: data.url, authorName: data.authorName });
|
||||
const nextLibItems = libraryItems.slice();
|
||||
nextLibItems.forEach((libItem) => {
|
||||
if (selectedItems.includes(libItem.id)) {
|
||||
libItem.status = "published";
|
||||
}
|
||||
});
|
||||
library.setLibrary(nextLibItems);
|
||||
},
|
||||
[setShowPublishLibraryDialog, setPublishLibSuccess, selectedItems, library],
|
||||
);
|
||||
const onPublishLibSuccess = (
|
||||
data: { url: string; authorName: string },
|
||||
libraryItems: LibraryItems,
|
||||
) => {
|
||||
setShowPublishLibraryDialog(false);
|
||||
setPublishLibSuccess({ url: data.url, authorName: data.authorName });
|
||||
const nextLibItems = libraryItems.slice();
|
||||
nextLibItems.forEach((libItem) => {
|
||||
if (selectedItems.includes(libItem.id)) {
|
||||
libItem.status = "published";
|
||||
}
|
||||
});
|
||||
library.setLibrary(nextLibItems);
|
||||
};
|
||||
|
||||
const onLibraryImport = async () => {
|
||||
try {
|
||||
|
@ -280,27 +281,29 @@ export const LibraryDropdownMenu = ({
|
|||
className?: string;
|
||||
}) => {
|
||||
const { library } = useApp();
|
||||
const { clearLibraryCache, deleteItemsFromLibraryCache } = useLibraryCache();
|
||||
const appState = useUIAppState();
|
||||
const setAppState = useExcalidrawSetAppState();
|
||||
|
||||
const [libraryItemsData] = useAtom(libraryItemsAtom, jotaiScope);
|
||||
|
||||
const removeFromLibrary = useCallback(
|
||||
async (libraryItems: LibraryItems) => {
|
||||
const nextItems = libraryItems.filter(
|
||||
(item) => !selectedItems.includes(item.id),
|
||||
);
|
||||
library.setLibrary(nextItems).catch(() => {
|
||||
setAppState({ errorMessage: t("alerts.errorRemovingFromLibrary") });
|
||||
});
|
||||
onSelectItems([]);
|
||||
},
|
||||
[library, setAppState, selectedItems, onSelectItems],
|
||||
);
|
||||
const removeFromLibrary = async (libraryItems: LibraryItems) => {
|
||||
const nextItems = libraryItems.filter(
|
||||
(item) => !selectedItems.includes(item.id),
|
||||
);
|
||||
library.setLibrary(nextItems).catch(() => {
|
||||
setAppState({ errorMessage: t("alerts.errorRemovingFromLibrary") });
|
||||
});
|
||||
|
||||
const resetLibrary = useCallback(() => {
|
||||
deleteItemsFromLibraryCache(selectedItems);
|
||||
|
||||
onSelectItems([]);
|
||||
};
|
||||
|
||||
const resetLibrary = () => {
|
||||
library.resetLibrary();
|
||||
}, [library]);
|
||||
clearLibraryCache();
|
||||
};
|
||||
|
||||
return (
|
||||
<LibraryDropdownMenuButton
|
||||
|
|
|
@ -73,6 +73,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
&__grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||
grid-gap: 1rem;
|
||||
}
|
||||
|
||||
.separator {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
import React, { useState } from "react";
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { serializeLibraryAsJSON } from "../data/json";
|
||||
import { ExcalidrawElement, NonDeleted } from "../element/types";
|
||||
import { t } from "../i18n";
|
||||
import {
|
||||
ExcalidrawProps,
|
||||
|
@ -8,202 +13,71 @@ import {
|
|||
LibraryItems,
|
||||
UIAppState,
|
||||
} from "../types";
|
||||
import { arrayToMap, chunk } from "../utils";
|
||||
import { LibraryUnit } from "./LibraryUnit";
|
||||
import { arrayToMap } from "../utils";
|
||||
import Stack from "./Stack";
|
||||
import { MIME_TYPES } from "../constants";
|
||||
import Spinner from "./Spinner";
|
||||
import { duplicateElements } from "../element/newElement";
|
||||
import { LibraryMenuControlButtons } from "./LibraryMenuControlButtons";
|
||||
import { LibraryDropdownMenu } from "./LibraryMenuHeaderContent";
|
||||
import {
|
||||
LibraryMenuSection,
|
||||
LibraryMenuSectionGrid,
|
||||
} from "./LibraryMenuSection";
|
||||
import { useScrollPosition } from "../hooks/useScrollPosition";
|
||||
import { useLibraryCache } from "../hooks/useLibraryItemSvg";
|
||||
|
||||
import "./LibraryMenuItems.scss";
|
||||
|
||||
const CELLS_PER_ROW = 4;
|
||||
// using an odd number of items per batch so the rendering creates an irregular
|
||||
// pattern which looks more organic
|
||||
const ITEMS_RENDERED_PER_BATCH = 17;
|
||||
// when render outputs cached we can render many more items per batch to
|
||||
// speed it up
|
||||
const CACHED_ITEMS_RENDERED_PER_BATCH = 64;
|
||||
|
||||
const LibraryMenuItems = ({
|
||||
export default function LibraryMenuItems({
|
||||
isLoading,
|
||||
libraryItems,
|
||||
onAddToLibrary,
|
||||
onInsertLibraryItems,
|
||||
pendingElements,
|
||||
selectedItems,
|
||||
onSelectItems,
|
||||
theme,
|
||||
id,
|
||||
libraryReturnUrl,
|
||||
onSelectItems,
|
||||
selectedItems,
|
||||
}: {
|
||||
isLoading: boolean;
|
||||
libraryItems: LibraryItems;
|
||||
pendingElements: LibraryItem["elements"];
|
||||
onInsertLibraryItems: (libraryItems: LibraryItems) => void;
|
||||
onAddToLibrary: (elements: LibraryItem["elements"]) => void;
|
||||
selectedItems: LibraryItem["id"][];
|
||||
onSelectItems: (id: LibraryItem["id"][]) => void;
|
||||
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
|
||||
theme: UIAppState["theme"];
|
||||
id: string;
|
||||
}) => {
|
||||
const [lastSelectedItem, setLastSelectedItem] = useState<
|
||||
LibraryItem["id"] | null
|
||||
>(null);
|
||||
selectedItems: LibraryItem["id"][];
|
||||
onSelectItems: (id: LibraryItem["id"][]) => void;
|
||||
}) {
|
||||
const libraryContainerRef = useRef<HTMLDivElement>(null);
|
||||
const scrollPosition = useScrollPosition<HTMLDivElement>(libraryContainerRef);
|
||||
|
||||
const onItemSelectToggle = (
|
||||
id: LibraryItem["id"],
|
||||
event: React.MouseEvent,
|
||||
) => {
|
||||
const shouldSelect = !selectedItems.includes(id);
|
||||
|
||||
const orderedItems = [...unpublishedItems, ...publishedItems];
|
||||
|
||||
if (shouldSelect) {
|
||||
if (event.shiftKey && lastSelectedItem) {
|
||||
const rangeStart = orderedItems.findIndex(
|
||||
(item) => item.id === lastSelectedItem,
|
||||
);
|
||||
const rangeEnd = orderedItems.findIndex((item) => item.id === id);
|
||||
|
||||
if (rangeStart === -1 || rangeEnd === -1) {
|
||||
onSelectItems([...selectedItems, id]);
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedItemsMap = arrayToMap(selectedItems);
|
||||
const nextSelectedIds = orderedItems.reduce(
|
||||
(acc: LibraryItem["id"][], item, idx) => {
|
||||
if (
|
||||
(idx >= rangeStart && idx <= rangeEnd) ||
|
||||
selectedItemsMap.has(item.id)
|
||||
) {
|
||||
acc.push(item.id);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
onSelectItems(nextSelectedIds);
|
||||
} else {
|
||||
onSelectItems([...selectedItems, id]);
|
||||
}
|
||||
setLastSelectedItem(id);
|
||||
} else {
|
||||
setLastSelectedItem(null);
|
||||
onSelectItems(selectedItems.filter((_id) => _id !== id));
|
||||
// This effect has to be called only on first render, therefore `scrollPosition` isn't in the dependency array
|
||||
useEffect(() => {
|
||||
if (scrollPosition > 0) {
|
||||
libraryContainerRef.current?.scrollTo(0, scrollPosition);
|
||||
}
|
||||
};
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const getInsertedElements = (id: string) => {
|
||||
let targetElements;
|
||||
if (selectedItems.includes(id)) {
|
||||
targetElements = libraryItems.filter((item) =>
|
||||
selectedItems.includes(item.id),
|
||||
);
|
||||
} else {
|
||||
targetElements = libraryItems.filter((item) => item.id === id);
|
||||
}
|
||||
return targetElements.map((item) => {
|
||||
return {
|
||||
...item,
|
||||
// duplicate each library item before inserting on canvas to confine
|
||||
// ids and bindings to each library item. See #6465
|
||||
elements: duplicateElements(item.elements, { randomizeSeed: true }),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const createLibraryItemCompo = (params: {
|
||||
item:
|
||||
| LibraryItem
|
||||
| /* pending library item */ {
|
||||
id: null;
|
||||
elements: readonly NonDeleted<ExcalidrawElement>[];
|
||||
}
|
||||
| null;
|
||||
onClick?: () => void;
|
||||
key: string;
|
||||
}) => {
|
||||
return (
|
||||
<Stack.Col key={params.key}>
|
||||
<LibraryUnit
|
||||
elements={params.item?.elements}
|
||||
isPending={!params.item?.id && !!params.item?.elements}
|
||||
onClick={params.onClick || (() => {})}
|
||||
id={params.item?.id || null}
|
||||
selected={!!params.item?.id && selectedItems.includes(params.item.id)}
|
||||
onToggle={onItemSelectToggle}
|
||||
onDrag={(id, event) => {
|
||||
event.dataTransfer.setData(
|
||||
MIME_TYPES.excalidrawlib,
|
||||
serializeLibraryAsJSON(getInsertedElements(id)),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Stack.Col>
|
||||
);
|
||||
};
|
||||
|
||||
const renderLibrarySection = (
|
||||
items: (
|
||||
| LibraryItem
|
||||
| /* pending library item */ {
|
||||
id: null;
|
||||
elements: readonly NonDeleted<ExcalidrawElement>[];
|
||||
}
|
||||
)[],
|
||||
) => {
|
||||
const _items = items.map((item) => {
|
||||
if (item.id) {
|
||||
return createLibraryItemCompo({
|
||||
item,
|
||||
onClick: () => onInsertLibraryItems(getInsertedElements(item.id)),
|
||||
key: item.id,
|
||||
});
|
||||
}
|
||||
return createLibraryItemCompo({
|
||||
key: "__pending__item__",
|
||||
item,
|
||||
onClick: () => onAddToLibrary(pendingElements),
|
||||
});
|
||||
});
|
||||
|
||||
// ensure we render all empty cells if no items are present
|
||||
let rows = chunk(_items, CELLS_PER_ROW);
|
||||
if (!rows.length) {
|
||||
rows = [[]];
|
||||
}
|
||||
|
||||
return rows.map((rowItems, index, rows) => {
|
||||
if (index === rows.length - 1) {
|
||||
// pad row with empty cells
|
||||
rowItems = rowItems.concat(
|
||||
new Array(CELLS_PER_ROW - rowItems.length)
|
||||
.fill(null)
|
||||
.map((_, index) => {
|
||||
return createLibraryItemCompo({
|
||||
key: `empty_${index}`,
|
||||
item: null,
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Stack.Row
|
||||
align="center"
|
||||
key={index}
|
||||
className="library-menu-items-container__row"
|
||||
>
|
||||
{rowItems}
|
||||
</Stack.Row>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const unpublishedItems = libraryItems.filter(
|
||||
(item) => item.status !== "published",
|
||||
const { svgCache } = useLibraryCache();
|
||||
const unpublishedItems = useMemo(
|
||||
() => libraryItems.filter((item) => item.status !== "published"),
|
||||
[libraryItems],
|
||||
);
|
||||
const publishedItems = libraryItems.filter(
|
||||
(item) => item.status === "published",
|
||||
|
||||
const publishedItems = useMemo(
|
||||
() => libraryItems.filter((item) => item.status === "published"),
|
||||
[libraryItems],
|
||||
);
|
||||
|
||||
const showBtn = !libraryItems.length && !pendingElements.length;
|
||||
|
@ -213,6 +87,122 @@ const LibraryMenuItems = ({
|
|||
!unpublishedItems.length &&
|
||||
!publishedItems.length;
|
||||
|
||||
const [lastSelectedItem, setLastSelectedItem] = useState<
|
||||
LibraryItem["id"] | null
|
||||
>(null);
|
||||
|
||||
const onItemSelectToggle = useCallback(
|
||||
(id: LibraryItem["id"], event: React.MouseEvent) => {
|
||||
const shouldSelect = !selectedItems.includes(id);
|
||||
|
||||
const orderedItems = [...unpublishedItems, ...publishedItems];
|
||||
|
||||
if (shouldSelect) {
|
||||
if (event.shiftKey && lastSelectedItem) {
|
||||
const rangeStart = orderedItems.findIndex(
|
||||
(item) => item.id === lastSelectedItem,
|
||||
);
|
||||
const rangeEnd = orderedItems.findIndex((item) => item.id === id);
|
||||
|
||||
if (rangeStart === -1 || rangeEnd === -1) {
|
||||
onSelectItems([...selectedItems, id]);
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedItemsMap = arrayToMap(selectedItems);
|
||||
const nextSelectedIds = orderedItems.reduce(
|
||||
(acc: LibraryItem["id"][], item, idx) => {
|
||||
if (
|
||||
(idx >= rangeStart && idx <= rangeEnd) ||
|
||||
selectedItemsMap.has(item.id)
|
||||
) {
|
||||
acc.push(item.id);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
onSelectItems(nextSelectedIds);
|
||||
} else {
|
||||
onSelectItems([...selectedItems, id]);
|
||||
}
|
||||
setLastSelectedItem(id);
|
||||
} else {
|
||||
setLastSelectedItem(null);
|
||||
onSelectItems(selectedItems.filter((_id) => _id !== id));
|
||||
}
|
||||
},
|
||||
[
|
||||
lastSelectedItem,
|
||||
onSelectItems,
|
||||
publishedItems,
|
||||
selectedItems,
|
||||
unpublishedItems,
|
||||
],
|
||||
);
|
||||
|
||||
const getInsertedElements = useCallback(
|
||||
(id: string) => {
|
||||
let targetElements;
|
||||
if (selectedItems.includes(id)) {
|
||||
targetElements = libraryItems.filter((item) =>
|
||||
selectedItems.includes(item.id),
|
||||
);
|
||||
} else {
|
||||
targetElements = libraryItems.filter((item) => item.id === id);
|
||||
}
|
||||
return targetElements.map((item) => {
|
||||
return {
|
||||
...item,
|
||||
// duplicate each library item before inserting on canvas to confine
|
||||
// ids and bindings to each library item. See #6465
|
||||
elements: duplicateElements(item.elements, { randomizeSeed: true }),
|
||||
};
|
||||
});
|
||||
},
|
||||
[libraryItems, selectedItems],
|
||||
);
|
||||
|
||||
const onItemDrag = useCallback(
|
||||
(id: LibraryItem["id"], event: React.DragEvent) => {
|
||||
event.dataTransfer.setData(
|
||||
MIME_TYPES.excalidrawlib,
|
||||
serializeLibraryAsJSON(getInsertedElements(id)),
|
||||
);
|
||||
},
|
||||
[getInsertedElements],
|
||||
);
|
||||
|
||||
const isItemSelected = useCallback(
|
||||
(id: LibraryItem["id"] | null) => {
|
||||
if (!id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return selectedItems.includes(id);
|
||||
},
|
||||
[selectedItems],
|
||||
);
|
||||
|
||||
const onAddToLibraryClick = useCallback(() => {
|
||||
onAddToLibrary(pendingElements);
|
||||
}, [pendingElements, onAddToLibrary]);
|
||||
|
||||
const onItemClick = useCallback(
|
||||
(id: LibraryItem["id"] | null) => {
|
||||
if (id) {
|
||||
onInsertLibraryItems(getInsertedElements(id));
|
||||
}
|
||||
},
|
||||
[getInsertedElements, onInsertLibraryItems],
|
||||
);
|
||||
|
||||
const itemsRenderedPerBatch =
|
||||
svgCache.size >= libraryItems.length
|
||||
? CACHED_ITEMS_RENDERED_PER_BATCH
|
||||
: ITEMS_RENDERED_PER_BATCH;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="library-menu-items-container"
|
||||
|
@ -239,6 +229,7 @@ const LibraryMenuItems = ({
|
|||
flex: publishedItems.length > 0 ? 1 : "0 1 auto",
|
||||
marginBottom: 0,
|
||||
}}
|
||||
ref={libraryContainerRef}
|
||||
>
|
||||
<>
|
||||
{!isLibraryEmpty && (
|
||||
|
@ -258,28 +249,41 @@ const LibraryMenuItems = ({
|
|||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
<div className="library-menu-items-private-library-container">
|
||||
{!pendingElements.length && !unpublishedItems.length ? (
|
||||
<div className="library-menu-items__no-items">
|
||||
<div className="library-menu-items__no-items__label">
|
||||
{t("library.noItems")}
|
||||
</div>
|
||||
<div className="library-menu-items__no-items__hint">
|
||||
{publishedItems.length > 0
|
||||
? t("library.hint_emptyPrivateLibrary")
|
||||
: t("library.hint_emptyLibrary")}
|
||||
</div>
|
||||
{!pendingElements.length && !unpublishedItems.length ? (
|
||||
<div className="library-menu-items__no-items">
|
||||
<div className="library-menu-items__no-items__label">
|
||||
{t("library.noItems")}
|
||||
</div>
|
||||
) : (
|
||||
renderLibrarySection([
|
||||
// append pending library item
|
||||
...(pendingElements.length
|
||||
? [{ id: null, elements: pendingElements }]
|
||||
: []),
|
||||
...unpublishedItems,
|
||||
])
|
||||
)}
|
||||
</div>
|
||||
<div className="library-menu-items__no-items__hint">
|
||||
{publishedItems.length > 0
|
||||
? t("library.hint_emptyPrivateLibrary")
|
||||
: t("library.hint_emptyLibrary")}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<LibraryMenuSectionGrid>
|
||||
{pendingElements.length > 0 && (
|
||||
<LibraryMenuSection
|
||||
itemsRenderedPerBatch={itemsRenderedPerBatch}
|
||||
items={[{ id: null, elements: pendingElements }]}
|
||||
onItemSelectToggle={onItemSelectToggle}
|
||||
onItemDrag={onItemDrag}
|
||||
onClick={onAddToLibraryClick}
|
||||
isItemSelected={isItemSelected}
|
||||
svgCache={svgCache}
|
||||
/>
|
||||
)}
|
||||
<LibraryMenuSection
|
||||
itemsRenderedPerBatch={itemsRenderedPerBatch}
|
||||
items={unpublishedItems}
|
||||
onItemSelectToggle={onItemSelectToggle}
|
||||
onItemDrag={onItemDrag}
|
||||
onClick={onItemClick}
|
||||
isItemSelected={isItemSelected}
|
||||
svgCache={svgCache}
|
||||
/>
|
||||
</LibraryMenuSectionGrid>
|
||||
)}
|
||||
</>
|
||||
|
||||
<>
|
||||
|
@ -291,7 +295,17 @@ const LibraryMenuItems = ({
|
|||
</div>
|
||||
)}
|
||||
{publishedItems.length > 0 ? (
|
||||
renderLibrarySection(publishedItems)
|
||||
<LibraryMenuSectionGrid>
|
||||
<LibraryMenuSection
|
||||
itemsRenderedPerBatch={itemsRenderedPerBatch}
|
||||
items={publishedItems}
|
||||
onItemSelectToggle={onItemSelectToggle}
|
||||
onItemDrag={onItemDrag}
|
||||
onClick={onItemClick}
|
||||
isItemSelected={isItemSelected}
|
||||
svgCache={svgCache}
|
||||
/>
|
||||
</LibraryMenuSectionGrid>
|
||||
) : unpublishedItems.length > 0 ? (
|
||||
<div
|
||||
style={{
|
||||
|
@ -325,6 +339,4 @@ const LibraryMenuItems = ({
|
|||
</Stack.Col>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LibraryMenuItems;
|
||||
}
|
||||
|
|
77
src/components/LibraryMenuSection.tsx
Normal file
77
src/components/LibraryMenuSection.tsx
Normal file
|
@ -0,0 +1,77 @@
|
|||
import React, { memo, ReactNode, useEffect, useState } from "react";
|
||||
import { EmptyLibraryUnit, LibraryUnit } from "./LibraryUnit";
|
||||
import { LibraryItem } from "../types";
|
||||
import { ExcalidrawElement, NonDeleted } from "../element/types";
|
||||
import { SvgCache } from "../hooks/useLibraryItemSvg";
|
||||
import { useTransition } from "../hooks/useTransition";
|
||||
|
||||
type LibraryOrPendingItem = (
|
||||
| LibraryItem
|
||||
| /* pending library item */ {
|
||||
id: null;
|
||||
elements: readonly NonDeleted<ExcalidrawElement>[];
|
||||
}
|
||||
)[];
|
||||
|
||||
interface Props {
|
||||
items: LibraryOrPendingItem;
|
||||
onClick: (id: LibraryItem["id"] | null) => void;
|
||||
onItemSelectToggle: (id: LibraryItem["id"], event: React.MouseEvent) => void;
|
||||
onItemDrag: (id: LibraryItem["id"], event: React.DragEvent) => void;
|
||||
isItemSelected: (id: LibraryItem["id"] | null) => boolean;
|
||||
svgCache: SvgCache;
|
||||
itemsRenderedPerBatch: number;
|
||||
}
|
||||
|
||||
export const LibraryMenuSectionGrid = ({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}) => {
|
||||
return <div className="library-menu-items-container__grid">{children}</div>;
|
||||
};
|
||||
|
||||
export const LibraryMenuSection = memo(
|
||||
({
|
||||
items,
|
||||
onItemSelectToggle,
|
||||
onItemDrag,
|
||||
isItemSelected,
|
||||
onClick,
|
||||
svgCache,
|
||||
itemsRenderedPerBatch,
|
||||
}: Props) => {
|
||||
const [, startTransition] = useTransition();
|
||||
const [index, setIndex] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (index < items.length) {
|
||||
startTransition(() => {
|
||||
setIndex(index + itemsRenderedPerBatch);
|
||||
});
|
||||
}
|
||||
}, [index, items.length, startTransition, itemsRenderedPerBatch]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{items.map((item, i) => {
|
||||
return i < index ? (
|
||||
<LibraryUnit
|
||||
elements={item?.elements}
|
||||
isPending={!item?.id && !!item?.elements}
|
||||
onClick={onClick}
|
||||
svgCache={svgCache}
|
||||
id={item?.id}
|
||||
selected={isItemSelected(item.id)}
|
||||
onToggle={onItemSelectToggle}
|
||||
onDrag={onItemDrag}
|
||||
key={item?.id ?? i}
|
||||
/>
|
||||
) : (
|
||||
<EmptyLibraryUnit key={i} />
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
},
|
||||
);
|
|
@ -20,6 +20,27 @@
|
|||
border-color: var(--color-primary);
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
&--skeleton {
|
||||
opacity: 0.5;
|
||||
background: linear-gradient(
|
||||
-45deg,
|
||||
var(--color-gray-10),
|
||||
var(--color-gray-20),
|
||||
var(--color-gray-10)
|
||||
);
|
||||
background-size: 200% 200%;
|
||||
animation: library-unit__skeleton-opacity-animation 0.2s linear;
|
||||
}
|
||||
}
|
||||
|
||||
&.theme--dark .library-unit--skeleton {
|
||||
background-image: linear-gradient(
|
||||
-45deg,
|
||||
var(--color-gray-100),
|
||||
var(--color-gray-80),
|
||||
var(--color-gray-100)
|
||||
);
|
||||
}
|
||||
|
||||
.library-unit__dragger {
|
||||
|
@ -142,4 +163,18 @@
|
|||
transform: scale(0.85);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes library-unit__skeleton-opacity-animation {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
75% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,108 +1,107 @@
|
|||
import clsx from "clsx";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { memo, useEffect, useRef, useState } from "react";
|
||||
import { useDevice } from "../components/App";
|
||||
import { exportToSvg } from "../packages/utils";
|
||||
import { LibraryItem } from "../types";
|
||||
import "./LibraryUnit.scss";
|
||||
import { CheckboxItem } from "./CheckboxItem";
|
||||
import { PlusIcon } from "./icons";
|
||||
import { COLOR_PALETTE } from "../colors";
|
||||
import { SvgCache, useLibraryItemSvg } from "../hooks/useLibraryItemSvg";
|
||||
|
||||
export const LibraryUnit = ({
|
||||
id,
|
||||
elements,
|
||||
isPending,
|
||||
onClick,
|
||||
selected,
|
||||
onToggle,
|
||||
onDrag,
|
||||
}: {
|
||||
id: LibraryItem["id"] | /** for pending item */ null;
|
||||
elements?: LibraryItem["elements"];
|
||||
isPending?: boolean;
|
||||
onClick: () => void;
|
||||
selected: boolean;
|
||||
onToggle: (id: string, event: React.MouseEvent) => void;
|
||||
onDrag: (id: string, event: React.DragEvent) => void;
|
||||
}) => {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
useEffect(() => {
|
||||
const node = ref.current;
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
export const LibraryUnit = memo(
|
||||
({
|
||||
id,
|
||||
elements,
|
||||
isPending,
|
||||
onClick,
|
||||
selected,
|
||||
onToggle,
|
||||
onDrag,
|
||||
svgCache,
|
||||
}: {
|
||||
id: LibraryItem["id"] | /** for pending item */ null;
|
||||
elements?: LibraryItem["elements"];
|
||||
isPending?: boolean;
|
||||
onClick: (id: LibraryItem["id"] | null) => void;
|
||||
selected: boolean;
|
||||
onToggle: (id: string, event: React.MouseEvent) => void;
|
||||
onDrag: (id: string, event: React.DragEvent) => void;
|
||||
svgCache: SvgCache;
|
||||
}) => {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
const svg = useLibraryItemSvg(id, elements, svgCache);
|
||||
|
||||
(async () => {
|
||||
if (!elements) {
|
||||
useEffect(() => {
|
||||
const node = ref.current;
|
||||
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
const svg = await exportToSvg({
|
||||
elements,
|
||||
appState: {
|
||||
exportBackground: false,
|
||||
viewBackgroundColor: COLOR_PALETTE.white,
|
||||
},
|
||||
files: null,
|
||||
});
|
||||
svg.querySelector(".style-fonts")?.remove();
|
||||
node.innerHTML = svg.outerHTML;
|
||||
})();
|
||||
|
||||
return () => {
|
||||
node.innerHTML = "";
|
||||
};
|
||||
}, [elements]);
|
||||
if (svg) {
|
||||
node.innerHTML = svg.outerHTML;
|
||||
}
|
||||
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const isMobile = useDevice().isMobile;
|
||||
const adder = isPending && (
|
||||
<div className="library-unit__adder">{PlusIcon}</div>
|
||||
);
|
||||
return () => {
|
||||
node.innerHTML = "";
|
||||
};
|
||||
}, [svg]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx("library-unit", {
|
||||
"library-unit__active": elements,
|
||||
"library-unit--hover": elements && isHovered,
|
||||
"library-unit--selected": selected,
|
||||
})}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const isMobile = useDevice().isMobile;
|
||||
const adder = isPending && (
|
||||
<div className="library-unit__adder">{PlusIcon}</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx("library-unit__dragger", {
|
||||
"library-unit__pulse": !!isPending,
|
||||
className={clsx("library-unit", {
|
||||
"library-unit__active": elements,
|
||||
"library-unit--hover": elements && isHovered,
|
||||
"library-unit--selected": selected,
|
||||
"library-unit--skeleton": !svg,
|
||||
})}
|
||||
ref={ref}
|
||||
draggable={!!elements}
|
||||
onClick={
|
||||
!!elements || !!isPending
|
||||
? (event) => {
|
||||
if (id && event.shiftKey) {
|
||||
onToggle(id, event);
|
||||
} else {
|
||||
onClick();
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
<div
|
||||
className={clsx("library-unit__dragger", {
|
||||
"library-unit__pulse": !!isPending,
|
||||
})}
|
||||
ref={ref}
|
||||
draggable={!!elements}
|
||||
onClick={
|
||||
!!elements || !!isPending
|
||||
? (event) => {
|
||||
if (id && event.shiftKey) {
|
||||
onToggle(id, event);
|
||||
} else {
|
||||
onClick(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onDragStart={(event) => {
|
||||
if (!id) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
: undefined
|
||||
}
|
||||
setIsHovered(false);
|
||||
onDrag(id, event);
|
||||
}}
|
||||
/>
|
||||
{adder}
|
||||
{id && elements && (isHovered || isMobile || selected) && (
|
||||
<CheckboxItem
|
||||
checked={selected}
|
||||
onChange={(checked, event) => onToggle(id, event)}
|
||||
className="library-unit__checkbox"
|
||||
onDragStart={(event) => {
|
||||
if (!id) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
setIsHovered(false);
|
||||
onDrag(id, event);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
{adder}
|
||||
{id && elements && (isHovered || isMobile || selected) && (
|
||||
<CheckboxItem
|
||||
checked={selected}
|
||||
onChange={(checked, event) => onToggle(id, event)}
|
||||
className="library-unit__checkbox"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export const EmptyLibraryUnit = () => (
|
||||
<div className="library-unit library-unit--skeleton" />
|
||||
);
|
||||
|
|
|
@ -24,13 +24,15 @@
|
|||
}
|
||||
|
||||
.Modal__background {
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1;
|
||||
background-color: rgba(#121212, 0.2);
|
||||
|
||||
animation: Modal__background__fade-in 0.125s linear forwards;
|
||||
}
|
||||
|
||||
.Modal__content {
|
||||
|
@ -65,14 +67,23 @@
|
|||
}
|
||||
}
|
||||
|
||||
@keyframes Modal__content_fade-in {
|
||||
@keyframes Modal__background__fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes Modal__content_fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import "./Modal.scss";
|
||||
|
||||
import React, { useState, useLayoutEffect, useRef } from "react";
|
||||
import React from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import clsx from "clsx";
|
||||
import { KEYS } from "../keys";
|
||||
import { useExcalidrawContainer, useDevice } from "./App";
|
||||
import { AppState } from "../types";
|
||||
import { THEME } from "../constants";
|
||||
import { useCreatePortalContainer } from "../hooks/useCreatePortalContainer";
|
||||
|
||||
export const Modal: React.FC<{
|
||||
className?: string;
|
||||
|
@ -17,8 +16,10 @@ export const Modal: React.FC<{
|
|||
theme?: AppState["theme"];
|
||||
closeOnClickOutside?: boolean;
|
||||
}> = (props) => {
|
||||
const { theme = THEME.LIGHT, closeOnClickOutside = true } = props;
|
||||
const modalRoot = useBodyRoot(theme);
|
||||
const { closeOnClickOutside = true } = props;
|
||||
const modalRoot = useCreatePortalContainer({
|
||||
className: "excalidraw-modal-container",
|
||||
});
|
||||
|
||||
if (!modalRoot) {
|
||||
return null;
|
||||
|
@ -44,7 +45,7 @@ export const Modal: React.FC<{
|
|||
<div
|
||||
className="Modal__background"
|
||||
onClick={closeOnClickOutside ? props.onCloseRequest : undefined}
|
||||
></div>
|
||||
/>
|
||||
<div
|
||||
className="Modal__content"
|
||||
style={{ "--max-width": `${props.maxWidth}px` }}
|
||||
|
@ -56,43 +57,3 @@ export const Modal: React.FC<{
|
|||
modalRoot,
|
||||
);
|
||||
};
|
||||
|
||||
const useBodyRoot = (theme: AppState["theme"]) => {
|
||||
const [div, setDiv] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
const device = useDevice();
|
||||
const isMobileRef = useRef(device.isMobile);
|
||||
isMobileRef.current = device.isMobile;
|
||||
|
||||
const { container: excalidrawContainer } = useExcalidrawContainer();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (div) {
|
||||
div.classList.toggle("excalidraw--mobile", device.isMobile);
|
||||
}
|
||||
}, [div, device.isMobile]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const isDarkTheme =
|
||||
!!excalidrawContainer?.classList.contains("theme--dark") ||
|
||||
theme === "dark";
|
||||
const div = document.createElement("div");
|
||||
|
||||
div.classList.add("excalidraw", "excalidraw-modal-container");
|
||||
div.classList.toggle("excalidraw--mobile", isMobileRef.current);
|
||||
|
||||
if (isDarkTheme) {
|
||||
div.classList.add("theme--dark");
|
||||
div.classList.add("theme--dark-background-none");
|
||||
}
|
||||
document.body.appendChild(div);
|
||||
|
||||
setDiv(div);
|
||||
|
||||
return () => {
|
||||
document.body.removeChild(div);
|
||||
};
|
||||
}, [excalidrawContainer, theme]);
|
||||
|
||||
return div;
|
||||
};
|
||||
|
|
|
@ -106,7 +106,7 @@ export const PasteChartDialog = ({
|
|||
|
||||
return (
|
||||
<Dialog
|
||||
small
|
||||
size="small"
|
||||
onCloseRequest={handleClose}
|
||||
title={t("labels.pasteCharts")}
|
||||
className={"PasteChartDialog"}
|
||||
|
|
|
@ -12,6 +12,7 @@ type Props = {
|
|||
onChange: (value: string) => void;
|
||||
label: string;
|
||||
isNameEditable: boolean;
|
||||
ignoreFocus?: boolean;
|
||||
};
|
||||
|
||||
export const ProjectName = (props: Props) => {
|
||||
|
@ -19,7 +20,9 @@ export const ProjectName = (props: Props) => {
|
|||
const [fileName, setFileName] = useState<string>(props.value);
|
||||
|
||||
const handleBlur = (event: any) => {
|
||||
focusNearestParent(event.target);
|
||||
if (!props.ignoreFocus) {
|
||||
focusNearestParent(event.target);
|
||||
}
|
||||
const value = event.target.value;
|
||||
if (value !== props.value) {
|
||||
props.onChange(value);
|
||||
|
|
100
src/components/RadioGroup.scss
Normal file
100
src/components/RadioGroup.scss
Normal file
|
@ -0,0 +1,100 @@
|
|||
@import "../css/variables.module";
|
||||
|
||||
.excalidraw {
|
||||
--RadioGroup-background: #ffffff;
|
||||
--RadioGroup-border: var(--color-gray-30);
|
||||
|
||||
--RadioGroup-choice-color-off: var(--color-primary);
|
||||
--RadioGroup-choice-color-off-hover: var(--color-primary-darkest);
|
||||
--RadioGroup-choice-background-off: white;
|
||||
--RadioGroup-choice-background-off-active: var(--color-gray-20);
|
||||
|
||||
--RadioGroup-choice-color-on: white;
|
||||
--RadioGroup-choice-background-on: var(--color-primary);
|
||||
--RadioGroup-choice-background-on-hover: var(--color-primary-darker);
|
||||
--RadioGroup-choice-background-on-active: var(--color-primary-darkest);
|
||||
|
||||
&.theme--dark {
|
||||
--RadioGroup-background: var(--color-gray-85);
|
||||
--RadioGroup-border: var(--color-gray-70);
|
||||
|
||||
--RadioGroup-choice-background-off: var(--color-gray-85);
|
||||
--RadioGroup-choice-background-off-active: var(--color-gray-70);
|
||||
--RadioGroup-choice-color-on: var(--color-gray-85);
|
||||
}
|
||||
|
||||
.RadioGroup {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
|
||||
padding: 3px;
|
||||
border-radius: 10px;
|
||||
|
||||
background: var(--RadioGroup-background);
|
||||
border: 1px solid var(--RadioGroup-border);
|
||||
|
||||
&__choice {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 24px;
|
||||
|
||||
color: var(--RadioGroup-choice-color-off);
|
||||
background: var(--RadioGroup-choice-background-off);
|
||||
|
||||
border-radius: 8px;
|
||||
|
||||
font-family: "Assistant";
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-size: 0.75rem;
|
||||
line-height: 100%;
|
||||
user-select: none;
|
||||
letter-spacing: 0.4px;
|
||||
|
||||
transition: all 75ms ease-out;
|
||||
|
||||
&:hover {
|
||||
color: var(--RadioGroup-choice-color-off-hover);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: var(--RadioGroup-choice-background-off-active);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--RadioGroup-choice-color-on);
|
||||
background: var(--RadioGroup-choice-background-on);
|
||||
|
||||
&:hover {
|
||||
background: var(--RadioGroup-choice-background-on-hover);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: var(--RadioGroup-choice-background-on-active);
|
||||
}
|
||||
}
|
||||
|
||||
& input {
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
border-radius: 8px;
|
||||
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
42
src/components/RadioGroup.tsx
Normal file
42
src/components/RadioGroup.tsx
Normal file
|
@ -0,0 +1,42 @@
|
|||
import clsx from "clsx";
|
||||
import "./RadioGroup.scss";
|
||||
|
||||
export type RadioGroupChoice<T> = {
|
||||
value: T;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export type RadioGroupProps<T> = {
|
||||
choices: RadioGroupChoice<T>[];
|
||||
value: T;
|
||||
onChange: (value: T) => void;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export const RadioGroup = function <T>({
|
||||
onChange,
|
||||
value,
|
||||
choices,
|
||||
name,
|
||||
}: RadioGroupProps<T>) {
|
||||
return (
|
||||
<div className="RadioGroup">
|
||||
{choices.map((choice) => (
|
||||
<div
|
||||
className={clsx("RadioGroup__choice", {
|
||||
active: choice.value === value,
|
||||
})}
|
||||
key={choice.label}
|
||||
>
|
||||
<input
|
||||
name={name}
|
||||
type="radio"
|
||||
checked={choice.value === value}
|
||||
onChange={() => onChange(choice.value)}
|
||||
/>
|
||||
{choice.label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -6,7 +6,6 @@ import React, {
|
|||
forwardRef,
|
||||
useImperativeHandle,
|
||||
useCallback,
|
||||
RefObject,
|
||||
} from "react";
|
||||
import { Island } from ".././Island";
|
||||
import { atom, useSetAtom } from "jotai";
|
||||
|
@ -27,38 +26,10 @@ import { SidebarTabTriggers } from "./SidebarTabTriggers";
|
|||
import { SidebarTabTrigger } from "./SidebarTabTrigger";
|
||||
import { SidebarTabs } from "./SidebarTabs";
|
||||
import { SidebarTab } from "./SidebarTab";
|
||||
import { useUIAppState } from "../../context/ui-appState";
|
||||
import { useOutsideClick } from "../../hooks/useOutsideClick";
|
||||
|
||||
import "./Sidebar.scss";
|
||||
import { useUIAppState } from "../../context/ui-appState";
|
||||
|
||||
// FIXME replace this with the implem from ColorPicker once it's merged
|
||||
const useOnClickOutside = (
|
||||
ref: RefObject<HTMLElement>,
|
||||
cb: (event: MouseEvent) => void,
|
||||
) => {
|
||||
useEffect(() => {
|
||||
const listener = (event: MouseEvent) => {
|
||||
if (!ref.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
event.target instanceof Element &&
|
||||
(ref.current.contains(event.target) ||
|
||||
!document.body.contains(event.target))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
cb(event);
|
||||
};
|
||||
document.addEventListener("pointerdown", listener, false);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("pointerdown", listener);
|
||||
};
|
||||
}, [ref, cb]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Flags whether the currently rendered Sidebar is docked or not, for use
|
||||
|
@ -133,7 +104,7 @@ export const SidebarInner = forwardRef(
|
|||
setAppState({ openSidebar: null });
|
||||
}, [setAppState]);
|
||||
|
||||
useOnClickOutside(
|
||||
useOutsideClick(
|
||||
islandRef,
|
||||
useCallback(
|
||||
(event) => {
|
||||
|
|
|
@ -15,6 +15,7 @@ $duration: 1.6s;
|
|||
|
||||
svg {
|
||||
animation: rotate $duration linear infinite;
|
||||
animation-delay: var(--spinner-delay);
|
||||
transform-origin: center center;
|
||||
}
|
||||
|
||||
|
|
|
@ -5,13 +5,26 @@ import "./Spinner.scss";
|
|||
const Spinner = ({
|
||||
size = "1em",
|
||||
circleWidth = 8,
|
||||
synchronized = false,
|
||||
}: {
|
||||
size?: string | number;
|
||||
circleWidth?: number;
|
||||
synchronized?: boolean;
|
||||
}) => {
|
||||
const mountTime = React.useRef(Date.now());
|
||||
const mountDelay = -(mountTime.current % 1600);
|
||||
|
||||
return (
|
||||
<div className="Spinner">
|
||||
<svg viewBox="0 0 100 100" style={{ width: size, height: size }}>
|
||||
<svg
|
||||
viewBox="0 0 100 100"
|
||||
style={{
|
||||
width: size,
|
||||
height: size,
|
||||
// fix for remounting causing spinner flicker
|
||||
["--spinner-delay" as any]: synchronized ? `${mountDelay}ms` : 0,
|
||||
}}
|
||||
>
|
||||
<circle
|
||||
cx="50"
|
||||
cy="50"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import "./Stack.scss";
|
||||
|
||||
import React from "react";
|
||||
import React, { forwardRef } from "react";
|
||||
import clsx from "clsx";
|
||||
|
||||
type StackProps = {
|
||||
|
@ -10,53 +10,52 @@ type StackProps = {
|
|||
justifyContent?: "center" | "space-around" | "space-between";
|
||||
className?: string | boolean;
|
||||
style?: React.CSSProperties;
|
||||
ref: React.RefObject<HTMLDivElement>;
|
||||
};
|
||||
|
||||
const RowStack = ({
|
||||
children,
|
||||
gap,
|
||||
align,
|
||||
justifyContent,
|
||||
className,
|
||||
style,
|
||||
}: StackProps) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx("Stack Stack_horizontal", className)}
|
||||
style={{
|
||||
"--gap": gap,
|
||||
alignItems: align,
|
||||
justifyContent,
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const RowStack = forwardRef(
|
||||
(
|
||||
{ children, gap, align, justifyContent, className, style }: StackProps,
|
||||
ref: React.ForwardedRef<HTMLDivElement>,
|
||||
) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx("Stack Stack_horizontal", className)}
|
||||
style={{
|
||||
"--gap": gap,
|
||||
alignItems: align,
|
||||
justifyContent,
|
||||
...style,
|
||||
}}
|
||||
ref={ref}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const ColStack = ({
|
||||
children,
|
||||
gap,
|
||||
align,
|
||||
justifyContent,
|
||||
className,
|
||||
style,
|
||||
}: StackProps) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx("Stack Stack_vertical", className)}
|
||||
style={{
|
||||
"--gap": gap,
|
||||
justifyItems: align,
|
||||
justifyContent,
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const ColStack = forwardRef(
|
||||
(
|
||||
{ children, gap, align, justifyContent, className, style }: StackProps,
|
||||
ref: React.ForwardedRef<HTMLDivElement>,
|
||||
) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx("Stack Stack_vertical", className)}
|
||||
style={{
|
||||
"--gap": gap,
|
||||
justifyItems: align,
|
||||
justifyContent,
|
||||
...style,
|
||||
}}
|
||||
ref={ref}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default {
|
||||
Row: RowStack,
|
||||
|
|
116
src/components/Switch.scss
Normal file
116
src/components/Switch.scss
Normal file
|
@ -0,0 +1,116 @@
|
|||
@import "../css/variables.module";
|
||||
|
||||
.excalidraw {
|
||||
--Switch-disabled-color: #d6d6d6;
|
||||
--Switch-track-background: white;
|
||||
--Switch-thumb-background: #3d3d3d;
|
||||
|
||||
&.theme--dark {
|
||||
--Switch-disabled-color: #5c5c5c;
|
||||
--Switch-track-background: #242424;
|
||||
--Switch-thumb-background: #b8b8b8;
|
||||
}
|
||||
|
||||
.Switch {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
|
||||
width: 40px;
|
||||
height: 20px;
|
||||
border-radius: 12px;
|
||||
|
||||
transition-property: background, border;
|
||||
transition-duration: 150ms;
|
||||
transition-timing-function: ease-out;
|
||||
|
||||
background: var(--Switch-track-background);
|
||||
border: 1px solid var(--Switch-disabled-color);
|
||||
|
||||
&:hover {
|
||||
background: var(--Switch-track-background);
|
||||
border: 1px solid #999999;
|
||||
}
|
||||
|
||||
&.toggled {
|
||||
background: var(--color-primary);
|
||||
border: 1px solid var(--color-primary);
|
||||
|
||||
&:hover {
|
||||
background: var(--color-primary-darker);
|
||||
border: 1px solid var(--color-primary-darker);
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
background: var(--Switch-track-background);
|
||||
border: 1px solid var(--Switch-disabled-color);
|
||||
|
||||
&.toggled {
|
||||
background: var(--Switch-disabled-color);
|
||||
border: 1px solid var(--Switch-disabled-color);
|
||||
}
|
||||
}
|
||||
|
||||
&:before {
|
||||
content: "";
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
|
||||
border-radius: 100%;
|
||||
transition: all 150ms ease-out;
|
||||
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
top: 4px;
|
||||
left: 4px;
|
||||
|
||||
background: var(--Switch-thumb-background);
|
||||
}
|
||||
|
||||
&:active:before {
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
&.toggled:before {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
left: 22px;
|
||||
top: 2px;
|
||||
|
||||
background: var(--Switch-track-background);
|
||||
}
|
||||
|
||||
&.toggled:active:before {
|
||||
width: 16px;
|
||||
left: 20px;
|
||||
}
|
||||
|
||||
&.disabled:before {
|
||||
background: var(--Switch-disabled-color);
|
||||
}
|
||||
|
||||
&.disabled.toggled:before {
|
||||
background: var(--color-gray-50);
|
||||
}
|
||||
|
||||
& input {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
|
||||
border-radius: 12px;
|
||||
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
&:disabled {
|
||||
cursor: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
38
src/components/Switch.tsx
Normal file
38
src/components/Switch.tsx
Normal file
|
@ -0,0 +1,38 @@
|
|||
import clsx from "clsx";
|
||||
|
||||
import "./Switch.scss";
|
||||
|
||||
export type SwitchProps = {
|
||||
name: string;
|
||||
checked: boolean;
|
||||
title?: string;
|
||||
onChange: (value: boolean) => void;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const Switch = ({
|
||||
title,
|
||||
name,
|
||||
checked,
|
||||
onChange,
|
||||
disabled = false,
|
||||
}: SwitchProps) => {
|
||||
return (
|
||||
<div className={clsx("Switch", { toggled: checked, disabled })}>
|
||||
<input
|
||||
name={name}
|
||||
id={name}
|
||||
title={title}
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
disabled={disabled}
|
||||
onChange={() => onChange(!checked)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === " ") {
|
||||
onChange(!checked);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
118
src/components/TextField.scss
Normal file
118
src/components/TextField.scss
Normal file
|
@ -0,0 +1,118 @@
|
|||
@import "../css/variables.module";
|
||||
|
||||
.excalidraw {
|
||||
--ExcTextField--color: var(--color-gray-80);
|
||||
--ExcTextField--label-color: var(--color-gray-80);
|
||||
--ExcTextField--background: white;
|
||||
--ExcTextField--readonly--background: var(--color-gray-10);
|
||||
--ExcTextField--readonly--color: var(--color-gray-80);
|
||||
--ExcTextField--border: var(--color-gray-40);
|
||||
--ExcTextField--border-hover: var(--color-gray-50);
|
||||
--ExcTextField--placeholder: var(--color-gray-40);
|
||||
|
||||
&.theme--dark {
|
||||
--ExcTextField--color: var(--color-gray-10);
|
||||
--ExcTextField--label-color: var(--color-gray-20);
|
||||
--ExcTextField--background: var(--color-gray-85);
|
||||
--ExcTextField--readonly--background: var(--color-gray-80);
|
||||
--ExcTextField--readonly--color: var(--color-gray-40);
|
||||
--ExcTextField--border: var(--color-gray-70);
|
||||
--ExcTextField--border-hover: var(--color-gray-60);
|
||||
--ExcTextField--placeholder: var(--color-gray-80);
|
||||
}
|
||||
|
||||
.ExcTextField {
|
||||
&--fullWidth {
|
||||
width: 100%;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-family: "Assistant";
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
line-height: 150%;
|
||||
|
||||
color: var(--ExcTextField--label-color);
|
||||
|
||||
margin-bottom: 0.25rem;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
&__input {
|
||||
box-sizing: border-box;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0 1rem;
|
||||
|
||||
height: 3rem;
|
||||
|
||||
background: var(--ExcTextField--background);
|
||||
border: 1px solid var(--ExcTextField--border);
|
||||
border-radius: 0.5rem;
|
||||
|
||||
&:not(&--readonly) {
|
||||
&:hover {
|
||||
border-color: var(--ExcTextField--border-hover);
|
||||
}
|
||||
|
||||
&:active,
|
||||
&:focus-within {
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
& input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
height: 1.5rem;
|
||||
|
||||
color: var(--ExcTextField--color);
|
||||
|
||||
font-family: "Assistant";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-size: 1rem;
|
||||
line-height: 150%;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
background: transparent;
|
||||
|
||||
width: 100%;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--ExcTextField--placeholder);
|
||||
}
|
||||
|
||||
&:not(:focus) {
|
||||
&:hover {
|
||||
background-color: initial;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: initial;
|
||||
box-shadow: initial;
|
||||
}
|
||||
}
|
||||
|
||||
&--readonly {
|
||||
background: var(--ExcTextField--readonly--background);
|
||||
border-color: transparent;
|
||||
|
||||
& input {
|
||||
color: var(--ExcTextField--readonly--color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
57
src/components/TextField.tsx
Normal file
57
src/components/TextField.tsx
Normal file
|
@ -0,0 +1,57 @@
|
|||
import { forwardRef, useRef, useImperativeHandle, KeyboardEvent } from "react";
|
||||
import clsx from "clsx";
|
||||
|
||||
import "./TextField.scss";
|
||||
|
||||
export type TextFieldProps = {
|
||||
value?: string;
|
||||
|
||||
onChange?: (value: string) => void;
|
||||
onClick?: () => void;
|
||||
onKeyDown?: (event: KeyboardEvent<HTMLInputElement>) => void;
|
||||
|
||||
readonly?: boolean;
|
||||
fullWidth?: boolean;
|
||||
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
};
|
||||
|
||||
export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
||||
(
|
||||
{ value, onChange, label, fullWidth, placeholder, readonly, onKeyDown },
|
||||
ref,
|
||||
) => {
|
||||
const innerRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
useImperativeHandle(ref, () => innerRef.current!);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx("ExcTextField", {
|
||||
"ExcTextField--fullWidth": fullWidth,
|
||||
})}
|
||||
onClick={() => {
|
||||
innerRef.current?.focus();
|
||||
}}
|
||||
>
|
||||
<div className="ExcTextField__label">{label}</div>
|
||||
<div
|
||||
className={clsx("ExcTextField__input", {
|
||||
"ExcTextField__input--readonly": readonly,
|
||||
})}
|
||||
>
|
||||
<input
|
||||
readOnly={readonly}
|
||||
type="text"
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
ref={innerRef}
|
||||
onChange={(event) => onChange?.(event.target.value)}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
|
@ -11,8 +11,6 @@
|
|||
top: auto;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 0.75rem;
|
||||
|
||||
.dropdown-menu-container {
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import { useOutsideClick } from "../../hooks/useOutsideClick";
|
||||
import { Island } from "../Island";
|
||||
|
||||
import { useDevice } from "../App";
|
||||
import clsx from "clsx";
|
||||
import Stack from "../Stack";
|
||||
import React from "react";
|
||||
import React, { useRef } from "react";
|
||||
import { DropdownMenuContentPropsContext } from "./common";
|
||||
import { useOutsideClick } from "../../hooks/useOutsideClick";
|
||||
|
||||
const MenuContent = ({
|
||||
children,
|
||||
|
@ -24,7 +23,9 @@ const MenuContent = ({
|
|||
style?: React.CSSProperties;
|
||||
}) => {
|
||||
const device = useDevice();
|
||||
const menuRef = useOutsideClick(() => {
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useOutsideClick(menuRef, () => {
|
||||
onClickOutside?.();
|
||||
});
|
||||
|
||||
|
|
|
@ -1550,3 +1550,69 @@ export const handIcon = createIcon(
|
|||
</g>,
|
||||
tablerIconProps,
|
||||
);
|
||||
|
||||
export const downloadIcon = createIcon(
|
||||
<>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2 -2v-2"></path>
|
||||
<path d="M7 11l5 5l5 -5"></path>
|
||||
<path d="M12 4l0 12"></path>
|
||||
</>,
|
||||
tablerIconProps,
|
||||
);
|
||||
|
||||
export const copyIcon = createIcon(
|
||||
<>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path>
|
||||
<path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path>
|
||||
</>,
|
||||
tablerIconProps,
|
||||
);
|
||||
|
||||
export const helpIcon = createIcon(
|
||||
<>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0"></path>
|
||||
<path d="M12 17l0 .01"></path>
|
||||
<path d="M12 13.5a1.5 1.5 0 0 1 1 -1.5a2.6 2.6 0 1 0 -3 -4"></path>
|
||||
</>,
|
||||
tablerIconProps,
|
||||
);
|
||||
|
||||
export const playerPlayIcon = createIcon(
|
||||
<>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M7 4v16l13 -8z"></path>
|
||||
</>,
|
||||
tablerIconProps,
|
||||
);
|
||||
|
||||
export const playerStopFilledIcon = createIcon(
|
||||
<>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path
|
||||
d="M17 4h-10a3 3 0 0 0 -3 3v10a3 3 0 0 0 3 3h10a3 3 0 0 0 3 -3v-10a3 3 0 0 0 -3 -3z"
|
||||
strokeWidth="0"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</>,
|
||||
tablerIconProps,
|
||||
);
|
||||
|
||||
export const tablerCheckIcon = createIcon(
|
||||
<>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M5 12l5 5l10 -10"></path>
|
||||
</>,
|
||||
tablerIconProps,
|
||||
);
|
||||
|
||||
export const eyeDropperIcon = createIcon(
|
||||
<g strokeWidth={1.25}>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M11 7l6 6"></path>
|
||||
<path d="M4 16l11.7 -11.7a1 1 0 0 1 1.4 0l2.6 2.6a1 1 0 0 1 0 1.4l-11.7 11.7h-4v-4z"></path>
|
||||
</g>,
|
||||
tablerIconProps,
|
||||
);
|
||||
|
|
|
@ -59,6 +59,7 @@ export enum EVENT {
|
|||
GESTURE_START = "gesturestart",
|
||||
GESTURE_CHANGE = "gesturechange",
|
||||
POINTER_MOVE = "pointermove",
|
||||
POINTER_DOWN = "pointerdown",
|
||||
POINTER_UP = "pointerup",
|
||||
STATE_CHANGE = "statechange",
|
||||
WHEEL = "wheel",
|
||||
|
|
|
@ -103,6 +103,8 @@
|
|||
|
||||
--color-danger: #db6965;
|
||||
--color-promo: #e70078;
|
||||
--color-success: #268029;
|
||||
--color-success-lighter: #cafccc;
|
||||
|
||||
--border-radius-md: 0.375rem;
|
||||
--border-radius-lg: 0.5rem;
|
||||
|
|
|
@ -22,6 +22,7 @@ import {
|
|||
DEFAULT_SIDEBAR,
|
||||
LIBRARY_SIDEBAR_TAB,
|
||||
} from "../constants";
|
||||
import { libraryItemSvgsCache } from "../hooks/useLibraryItemSvg";
|
||||
|
||||
export const libraryItemsAtom = atom<{
|
||||
status: "loading" | "loaded";
|
||||
|
@ -115,6 +116,20 @@ class Library {
|
|||
}
|
||||
};
|
||||
|
||||
/** call on excalidraw instance unmount */
|
||||
destroy = () => {
|
||||
this.isInitialized = false;
|
||||
this.updateQueue = [];
|
||||
this.lastLibraryItems = [];
|
||||
jotaiStore.set(libraryItemSvgsCache, new Map());
|
||||
// TODO uncomment after/if we make jotai store scoped to each excal instance
|
||||
// jotaiStore.set(libraryItemsAtom, {
|
||||
// status: "loading",
|
||||
// isInitialized: false,
|
||||
// libraryItems: [],
|
||||
// });
|
||||
};
|
||||
|
||||
resetLibrary = () => {
|
||||
return this.setLibrary([]);
|
||||
};
|
||||
|
|
|
@ -14,17 +14,21 @@ import {
|
|||
NonDeleted,
|
||||
ExcalidrawElement,
|
||||
ExcalidrawTextElementWithContainer,
|
||||
ExcalidrawImageElement,
|
||||
} from "./types";
|
||||
import type { Mutable } from "../utility-types";
|
||||
import {
|
||||
getElementAbsoluteCoords,
|
||||
getCommonBounds,
|
||||
getResizedElementAbsoluteCoords,
|
||||
getCommonBoundingBox,
|
||||
getElementPointsCoords,
|
||||
} from "./bounds";
|
||||
import {
|
||||
isArrowElement,
|
||||
isBoundToContainer,
|
||||
isFreeDrawElement,
|
||||
isImageElement,
|
||||
isLinearElement,
|
||||
isTextElement,
|
||||
} from "./typeChecks";
|
||||
|
@ -49,8 +53,12 @@ import {
|
|||
measureText,
|
||||
getBoundTextMaxHeight,
|
||||
} from "./textElement";
|
||||
import { LinearElementEditor } from "./linearElementEditor";
|
||||
|
||||
export const normalizeAngle = (angle: number): number => {
|
||||
if (angle < 0) {
|
||||
return angle + 2 * Math.PI;
|
||||
}
|
||||
if (angle >= 2 * Math.PI) {
|
||||
return angle - 2 * Math.PI;
|
||||
}
|
||||
|
@ -596,7 +604,7 @@ export const resizeSingleElement = (
|
|||
}
|
||||
};
|
||||
|
||||
const resizeMultipleElements = (
|
||||
export const resizeMultipleElements = (
|
||||
pointerDownState: PointerDownState,
|
||||
selectedElements: readonly NonDeletedExcalidrawElement[],
|
||||
transformHandleType: "nw" | "ne" | "sw" | "se",
|
||||
|
@ -627,8 +635,28 @@ const resizeMultipleElements = (
|
|||
[],
|
||||
);
|
||||
|
||||
// getCommonBoundingBox() uses getBoundTextElement() which returns null for
|
||||
// original elements from pointerDownState, so we have to find and add these
|
||||
// bound text elements manually. Additionally, the coordinates of bound text
|
||||
// elements aren't always up to date.
|
||||
const boundTextElements = targetElements.reduce((acc, { orig }) => {
|
||||
if (!isLinearElement(orig)) {
|
||||
return acc;
|
||||
}
|
||||
const textId = getBoundTextElementId(orig);
|
||||
if (!textId) {
|
||||
return acc;
|
||||
}
|
||||
const text = pointerDownState.originalElements.get(textId) ?? null;
|
||||
if (!isBoundToContainer(text)) {
|
||||
return acc;
|
||||
}
|
||||
const xy = LinearElementEditor.getBoundTextElementPosition(orig, text);
|
||||
return [...acc, { ...text, ...xy }];
|
||||
}, [] as ExcalidrawTextElementWithContainer[]);
|
||||
|
||||
const { minX, minY, maxX, maxY, midX, midY } = getCommonBoundingBox(
|
||||
targetElements.map(({ orig }) => orig),
|
||||
targetElements.map(({ orig }) => orig).concat(boundTextElements),
|
||||
);
|
||||
const direction = transformHandleType;
|
||||
|
||||
|
@ -640,12 +668,22 @@ const resizeMultipleElements = (
|
|||
};
|
||||
|
||||
// anchor point must be on the opposite side of the dragged selection handle
|
||||
// or be the center of the selection if alt is pressed
|
||||
// or be the center of the selection if shouldResizeFromCenter
|
||||
const [anchorX, anchorY]: Point = shouldResizeFromCenter
|
||||
? [midX, midY]
|
||||
: mapDirectionsToAnchors[direction];
|
||||
|
||||
const mapDirectionsToPointerSides: Record<
|
||||
const scale =
|
||||
Math.max(
|
||||
Math.abs(pointerX - anchorX) / (maxX - minX) || 0,
|
||||
Math.abs(pointerY - anchorY) / (maxY - minY) || 0,
|
||||
) * (shouldResizeFromCenter ? 2 : 1);
|
||||
|
||||
if (scale === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mapDirectionsToPointerPositions: Record<
|
||||
typeof direction,
|
||||
[x: boolean, y: boolean]
|
||||
> = {
|
||||
|
@ -655,68 +693,117 @@ const resizeMultipleElements = (
|
|||
nw: [pointerX <= anchorX, pointerY <= anchorY],
|
||||
};
|
||||
|
||||
// pointer side relative to anchor
|
||||
const [pointerSideX, pointerSideY] = mapDirectionsToPointerSides[
|
||||
/**
|
||||
* to flip an element:
|
||||
* 1. determine over which axis is the element being flipped
|
||||
* (could be x, y, or both) indicated by `flipFactorX` & `flipFactorY`
|
||||
* 2. shift element's position by the amount of width or height (or both) or
|
||||
* mirror points in the case of linear & freedraw elemenets
|
||||
* 3. adjust element angle
|
||||
*/
|
||||
const [flipFactorX, flipFactorY] = mapDirectionsToPointerPositions[
|
||||
direction
|
||||
].map((condition) => (condition ? 1 : -1));
|
||||
const isFlippedByX = flipFactorX < 0;
|
||||
const isFlippedByY = flipFactorY < 0;
|
||||
|
||||
// stop resizing if a pointer is on the other side of selection
|
||||
if (pointerSideX < 0 && pointerSideY < 0) {
|
||||
return;
|
||||
}
|
||||
const elementsAndUpdates: {
|
||||
element: NonDeletedExcalidrawElement;
|
||||
update: Mutable<
|
||||
Pick<ExcalidrawElement, "x" | "y" | "width" | "height" | "angle">
|
||||
> & {
|
||||
points?: ExcalidrawLinearElement["points"];
|
||||
fontSize?: ExcalidrawTextElement["fontSize"];
|
||||
baseline?: ExcalidrawTextElement["baseline"];
|
||||
scale?: ExcalidrawImageElement["scale"];
|
||||
};
|
||||
boundText: {
|
||||
element: ExcalidrawTextElementWithContainer;
|
||||
fontSize: ExcalidrawTextElement["fontSize"];
|
||||
baseline: ExcalidrawTextElement["baseline"];
|
||||
} | null;
|
||||
}[] = [];
|
||||
|
||||
const scale =
|
||||
Math.max(
|
||||
(pointerSideX * Math.abs(pointerX - anchorX)) / (maxX - minX),
|
||||
(pointerSideY * Math.abs(pointerY - anchorY)) / (maxY - minY),
|
||||
) * (shouldResizeFromCenter ? 2 : 1);
|
||||
for (const { orig, latest } of targetElements) {
|
||||
// bounded text elements are updated along with their container elements
|
||||
if (isTextElement(orig) && isBoundToContainer(orig)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (scale === 0) {
|
||||
return;
|
||||
}
|
||||
const width = orig.width * scale;
|
||||
const height = orig.height * scale;
|
||||
const angle = normalizeAngle(orig.angle * flipFactorX * flipFactorY);
|
||||
|
||||
targetElements.forEach((element) => {
|
||||
const width = element.orig.width * scale;
|
||||
const height = element.orig.height * scale;
|
||||
const x = anchorX + (element.orig.x - anchorX) * scale;
|
||||
const y = anchorY + (element.orig.y - anchorY) * scale;
|
||||
const isLinearOrFreeDraw = isLinearElement(orig) || isFreeDrawElement(orig);
|
||||
const offsetX = orig.x - anchorX;
|
||||
const offsetY = orig.y - anchorY;
|
||||
const shiftX = isFlippedByX && !isLinearOrFreeDraw ? width : 0;
|
||||
const shiftY = isFlippedByY && !isLinearOrFreeDraw ? height : 0;
|
||||
const x = anchorX + flipFactorX * (offsetX * scale + shiftX);
|
||||
const y = anchorY + flipFactorY * (offsetY * scale + shiftY);
|
||||
|
||||
// readjust points for linear & free draw elements
|
||||
const rescaledPoints = rescalePointsInElement(
|
||||
element.orig,
|
||||
width,
|
||||
height,
|
||||
orig,
|
||||
width * flipFactorX,
|
||||
height * flipFactorY,
|
||||
false,
|
||||
);
|
||||
|
||||
const update: {
|
||||
width: number;
|
||||
height: number;
|
||||
x: number;
|
||||
y: number;
|
||||
points?: Point[];
|
||||
fontSize?: number;
|
||||
baseline?: number;
|
||||
} = {
|
||||
width,
|
||||
height,
|
||||
const update: typeof elementsAndUpdates[0]["update"] = {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
angle,
|
||||
...rescaledPoints,
|
||||
};
|
||||
|
||||
let boundTextUpdates: { fontSize: number; baseline: number } | null = null;
|
||||
if (isImageElement(orig) && targetElements.length === 1) {
|
||||
update.scale = [orig.scale[0] * flipFactorX, orig.scale[1] * flipFactorY];
|
||||
}
|
||||
|
||||
const boundTextElement = getBoundTextElement(element.latest);
|
||||
if (isLinearElement(orig) && (isFlippedByX || isFlippedByY)) {
|
||||
const origBounds = getElementPointsCoords(orig, orig.points);
|
||||
const newBounds = getElementPointsCoords(
|
||||
{ ...orig, x, y },
|
||||
rescaledPoints.points!,
|
||||
);
|
||||
const origXY = [orig.x, orig.y];
|
||||
const newXY = [x, y];
|
||||
|
||||
if (boundTextElement || isTextElement(element.orig)) {
|
||||
const linearShift = (axis: "x" | "y") => {
|
||||
const i = axis === "x" ? 0 : 1;
|
||||
return (
|
||||
(newBounds[i + 2] -
|
||||
newXY[i] -
|
||||
(origXY[i] - origBounds[i]) * scale +
|
||||
(origBounds[i + 2] - origXY[i]) * scale -
|
||||
(newXY[i] - newBounds[i])) /
|
||||
2
|
||||
);
|
||||
};
|
||||
|
||||
if (isFlippedByX) {
|
||||
update.x -= linearShift("x");
|
||||
}
|
||||
|
||||
if (isFlippedByY) {
|
||||
update.y -= linearShift("y");
|
||||
}
|
||||
}
|
||||
|
||||
let boundText: typeof elementsAndUpdates[0]["boundText"] = null;
|
||||
|
||||
const boundTextElement = getBoundTextElement(latest);
|
||||
|
||||
if (boundTextElement || isTextElement(orig)) {
|
||||
const updatedElement = {
|
||||
...element.latest,
|
||||
...latest,
|
||||
width,
|
||||
height,
|
||||
};
|
||||
const metrics = measureFontSizeFromWidth(
|
||||
boundTextElement ?? (element.orig as ExcalidrawTextElement),
|
||||
boundTextElement ?? (orig as ExcalidrawTextElement),
|
||||
boundTextElement
|
||||
? getBoundTextMaxWidth(updatedElement)
|
||||
: updatedElement.width,
|
||||
|
@ -729,29 +816,50 @@ const resizeMultipleElements = (
|
|||
return;
|
||||
}
|
||||
|
||||
if (isTextElement(element.orig)) {
|
||||
if (isTextElement(orig)) {
|
||||
update.fontSize = metrics.size;
|
||||
update.baseline = metrics.baseline;
|
||||
}
|
||||
|
||||
if (boundTextElement) {
|
||||
boundTextUpdates = {
|
||||
boundText = {
|
||||
element: boundTextElement,
|
||||
fontSize: metrics.size,
|
||||
baseline: metrics.baseline,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
updateBoundElements(element.latest, { newSize: { width, height } });
|
||||
elementsAndUpdates.push({ element: latest, update, boundText });
|
||||
}
|
||||
|
||||
mutateElement(element.latest, update);
|
||||
const elementsToUpdate = elementsAndUpdates.map(({ element }) => element);
|
||||
|
||||
if (boundTextElement && boundTextUpdates) {
|
||||
mutateElement(boundTextElement, boundTextUpdates);
|
||||
for (const { element, update, boundText } of elementsAndUpdates) {
|
||||
const { width, height, angle } = update;
|
||||
|
||||
handleBindTextResize(element.latest, transformHandleType);
|
||||
mutateElement(element, update, false);
|
||||
|
||||
updateBoundElements(element, {
|
||||
simultaneouslyUpdated: elementsToUpdate,
|
||||
newSize: { width, height },
|
||||
});
|
||||
|
||||
if (boundText) {
|
||||
const { element: boundTextElement, ...boundTextUpdates } = boundText;
|
||||
mutateElement(
|
||||
boundTextElement,
|
||||
{
|
||||
...boundTextUpdates,
|
||||
angle: isLinearElement(element) ? undefined : angle,
|
||||
},
|
||||
false,
|
||||
);
|
||||
handleBindTextResize(element, transformHandleType);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Scene.getScene(elementsAndUpdates[0].element)?.informMutation();
|
||||
};
|
||||
|
||||
const rotateMultipleElements = (
|
||||
|
|
|
@ -834,7 +834,6 @@ class Collab extends PureComponent<Props, CollabState> {
|
|||
setErrorMessage={(errorMessage) => {
|
||||
this.setState({ errorMessage });
|
||||
}}
|
||||
theme={this.excalidrawAPI.getAppState().theme}
|
||||
/>
|
||||
)}
|
||||
{errorMessage && (
|
||||
|
|
|
@ -1,76 +1,149 @@
|
|||
@import "../../css/variables.module";
|
||||
|
||||
.excalidraw {
|
||||
.RoomDialog__button {
|
||||
border: 1px solid var(--default-border-color) !important;
|
||||
}
|
||||
|
||||
.RoomDialog-linkContainer {
|
||||
.RoomDialog {
|
||||
display: flex;
|
||||
margin: 1.5em 0;
|
||||
}
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
|
||||
input.RoomDialog-link {
|
||||
color: var(--text-primary-color);
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
margin-inline-start: 1em;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
padding: 0 0.5rem;
|
||||
white-space: nowrap;
|
||||
border-radius: var(--space-factor);
|
||||
background-color: var(--button-gray-1);
|
||||
}
|
||||
|
||||
.RoomDialog-emoji {
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
.RoomDialog-usernameContainer {
|
||||
display: flex;
|
||||
margin: 1.5em 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@include isMobile {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
height: calc(100vh - 5rem);
|
||||
}
|
||||
}
|
||||
|
||||
@include isMobile {
|
||||
.RoomDialog-usernameLabel {
|
||||
font-weight: bold;
|
||||
&__popover {
|
||||
@keyframes RoomDialog__popover__scaleIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
box-sizing: border-box;
|
||||
z-index: 100;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
padding: 0.125rem 0.5rem;
|
||||
gap: 0.125rem;
|
||||
|
||||
height: 1.125rem;
|
||||
|
||||
border: none;
|
||||
border-radius: 0.6875rem;
|
||||
|
||||
font-family: "Assistant";
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-size: 0.75rem;
|
||||
line-height: 110%;
|
||||
|
||||
background: var(--color-success-lighter);
|
||||
color: var(--color-success);
|
||||
|
||||
& > svg {
|
||||
width: 0.875rem;
|
||||
height: 0.875rem;
|
||||
}
|
||||
|
||||
transform-origin: var(--radix-popover-content-transform-origin);
|
||||
animation: RoomDialog__popover__scaleIn 150ms ease-out;
|
||||
}
|
||||
}
|
||||
|
||||
.RoomDialog-username {
|
||||
background-color: var(--input-bg-color);
|
||||
border-color: var(--input-border-color);
|
||||
appearance: none;
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
margin-inline-start: 1em;
|
||||
@include isMobile {
|
||||
margin-top: 0.5em;
|
||||
margin-inline-start: 0;
|
||||
&__inactive {
|
||||
font-family: "Assistant";
|
||||
|
||||
&__illustration {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
& svg {
|
||||
filter: var(--theme-filter);
|
||||
}
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
font-weight: 700;
|
||||
font-size: 1.3125rem;
|
||||
line-height: 130%;
|
||||
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
&__description {
|
||||
font-weight: 400;
|
||||
font-size: 0.875rem;
|
||||
line-height: 150%;
|
||||
|
||||
text-align: center;
|
||||
|
||||
color: var(--text-primary-color);
|
||||
|
||||
& strong {
|
||||
display: block;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
&__start_session {
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.RoomDialog-sessionStartButtonContainer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
&__active {
|
||||
&__share {
|
||||
display: none !important;
|
||||
|
||||
.Modal .RoomDialog-stopSession {
|
||||
background-color: var(--button-destructive-bg-color);
|
||||
@include isMobile {
|
||||
display: flex !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ToolIcon__label,
|
||||
.ToolIcon__icon svg {
|
||||
color: var(--button-destructive-color);
|
||||
&__header {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__linkRow {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-end;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
&__description {
|
||||
border-top: 1px solid var(--color-gray-20);
|
||||
|
||||
padding: 0.5rem 0.5rem 0;
|
||||
font-weight: 400;
|
||||
font-size: 0.75rem;
|
||||
line-height: 150%;
|
||||
|
||||
& p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
& p + p {
|
||||
margin-top: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,23 +1,27 @@
|
|||
import React, { useRef } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
import * as Popover from "@radix-ui/react-popover";
|
||||
|
||||
import { copyTextToSystemClipboard } from "../../clipboard";
|
||||
import { trackEvent } from "../../analytics";
|
||||
import { getFrame } from "../../utils";
|
||||
import { useI18n } from "../../i18n";
|
||||
import { KEYS } from "../../keys";
|
||||
|
||||
import { Dialog } from "../../components/Dialog";
|
||||
import {
|
||||
clipboard,
|
||||
start,
|
||||
stop,
|
||||
copyIcon,
|
||||
playerPlayIcon,
|
||||
playerStopFilledIcon,
|
||||
share,
|
||||
shareIOS,
|
||||
shareWindows,
|
||||
tablerCheckIcon,
|
||||
} from "../../components/icons";
|
||||
import { ToolButton } from "../../components/ToolButton";
|
||||
import { TextField } from "../../components/TextField";
|
||||
import { FilledButton } from "../../components/FilledButton";
|
||||
|
||||
import { ReactComponent as CollabImage } from "../../assets/lock.svg";
|
||||
import "./RoomDialog.scss";
|
||||
import Stack from "../../components/Stack";
|
||||
import { AppState } from "../../types";
|
||||
import { trackEvent } from "../../analytics";
|
||||
import { getFrame } from "../../utils";
|
||||
import DialogActionButton from "../../components/DialogActionButton";
|
||||
import { useI18n } from "../../i18n";
|
||||
import { KEYS } from "../../keys";
|
||||
|
||||
const getShareIcon = () => {
|
||||
const navigator = window.navigator as any;
|
||||
|
@ -33,16 +37,7 @@ const getShareIcon = () => {
|
|||
return share;
|
||||
};
|
||||
|
||||
const RoomDialog = ({
|
||||
handleClose,
|
||||
activeRoomLink,
|
||||
username,
|
||||
onUsernameChange,
|
||||
onRoomCreate,
|
||||
onRoomDestroy,
|
||||
setErrorMessage,
|
||||
theme,
|
||||
}: {
|
||||
export type RoomModalProps = {
|
||||
handleClose: () => void;
|
||||
activeRoomLink: string;
|
||||
username: string;
|
||||
|
@ -50,20 +45,41 @@ const RoomDialog = ({
|
|||
onRoomCreate: () => void;
|
||||
onRoomDestroy: () => void;
|
||||
setErrorMessage: (message: string) => void;
|
||||
theme: AppState["theme"];
|
||||
}) => {
|
||||
};
|
||||
|
||||
export const RoomModal = ({
|
||||
activeRoomLink,
|
||||
onRoomCreate,
|
||||
onRoomDestroy,
|
||||
setErrorMessage,
|
||||
username,
|
||||
onUsernameChange,
|
||||
handleClose,
|
||||
}: RoomModalProps) => {
|
||||
const { t } = useI18n();
|
||||
const roomLinkInput = useRef<HTMLInputElement>(null);
|
||||
const [justCopied, setJustCopied] = useState(false);
|
||||
const timerRef = useRef<number>(0);
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
const isShareSupported = "share" in navigator;
|
||||
|
||||
const copyRoomLink = async () => {
|
||||
try {
|
||||
await copyTextToSystemClipboard(activeRoomLink);
|
||||
|
||||
setJustCopied(true);
|
||||
|
||||
if (timerRef.current) {
|
||||
window.clearTimeout(timerRef.current);
|
||||
}
|
||||
|
||||
timerRef.current = window.setTimeout(() => {
|
||||
setJustCopied(false);
|
||||
}, 3000);
|
||||
} catch (error: any) {
|
||||
setErrorMessage(error.message);
|
||||
}
|
||||
if (roomLinkInput.current) {
|
||||
roomLinkInput.current.select();
|
||||
}
|
||||
|
||||
ref.current?.select();
|
||||
};
|
||||
|
||||
const shareRoomLink = async () => {
|
||||
|
@ -78,114 +94,124 @@ const RoomDialog = ({
|
|||
}
|
||||
};
|
||||
|
||||
const selectInput = (event: React.MouseEvent<HTMLInputElement>) => {
|
||||
if (event.target !== document.activeElement) {
|
||||
event.preventDefault();
|
||||
(event.target as HTMLInputElement).select();
|
||||
}
|
||||
};
|
||||
|
||||
const renderRoomDialog = () => {
|
||||
if (activeRoomLink) {
|
||||
return (
|
||||
<div className="RoomDialog-modal">
|
||||
{!activeRoomLink && (
|
||||
<>
|
||||
<p>{t("roomDialog.desc_intro")}</p>
|
||||
<p>{`🔒 ${t("roomDialog.desc_privacy")}`}</p>
|
||||
<div className="RoomDialog-sessionStartButtonContainer">
|
||||
<DialogActionButton
|
||||
label={t("roomDialog.button_startSession")}
|
||||
onClick={() => {
|
||||
trackEvent("share", "room creation", `ui (${getFrame()})`);
|
||||
onRoomCreate();
|
||||
}}
|
||||
>
|
||||
{start}
|
||||
</DialogActionButton>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{activeRoomLink && (
|
||||
<>
|
||||
<p>{t("roomDialog.desc_inProgressIntro")}</p>
|
||||
<p>{t("roomDialog.desc_shareLink")}</p>
|
||||
<div className="RoomDialog-linkContainer">
|
||||
<Stack.Row gap={2}>
|
||||
{"share" in navigator ? (
|
||||
<ToolButton
|
||||
className="RoomDialog__button"
|
||||
type="button"
|
||||
icon={getShareIcon()}
|
||||
title={t("labels.share")}
|
||||
aria-label={t("labels.share")}
|
||||
onClick={shareRoomLink}
|
||||
/>
|
||||
) : null}
|
||||
<ToolButton
|
||||
className="RoomDialog__button"
|
||||
type="button"
|
||||
icon={clipboard}
|
||||
title={t("labels.copy")}
|
||||
aria-label={t("labels.copy")}
|
||||
onClick={copyRoomLink}
|
||||
/>
|
||||
</Stack.Row>
|
||||
<input
|
||||
type="text"
|
||||
value={activeRoomLink}
|
||||
readOnly={true}
|
||||
className="RoomDialog-link"
|
||||
ref={roomLinkInput}
|
||||
onPointerDown={selectInput}
|
||||
<>
|
||||
<h3 className="RoomDialog__active__header">
|
||||
{t("labels.liveCollaboration")}
|
||||
</h3>
|
||||
<TextField
|
||||
value={username}
|
||||
placeholder="Your name"
|
||||
label="Your name"
|
||||
onChange={onUsernameChange}
|
||||
onKeyDown={(event) => event.key === KEYS.ENTER && handleClose()}
|
||||
/>
|
||||
<div className="RoomDialog__active__linkRow">
|
||||
<TextField
|
||||
ref={ref}
|
||||
label="Link"
|
||||
readonly
|
||||
fullWidth
|
||||
value={activeRoomLink}
|
||||
/>
|
||||
{isShareSupported && (
|
||||
<FilledButton
|
||||
size="large"
|
||||
variant="icon"
|
||||
label="Share"
|
||||
startIcon={getShareIcon()}
|
||||
className="RoomDialog__active__share"
|
||||
onClick={shareRoomLink}
|
||||
/>
|
||||
)}
|
||||
<Popover.Root open={justCopied}>
|
||||
<Popover.Trigger asChild>
|
||||
<FilledButton
|
||||
size="large"
|
||||
label="Copy link"
|
||||
startIcon={copyIcon}
|
||||
onClick={copyRoomLink}
|
||||
/>
|
||||
</div>
|
||||
<div className="RoomDialog-usernameContainer">
|
||||
<label className="RoomDialog-usernameLabel" htmlFor="username">
|
||||
{t("labels.yourName")}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
value={username.trim() || ""}
|
||||
className="RoomDialog-username TextInput"
|
||||
onChange={(event) => onUsernameChange(event.target.value)}
|
||||
onKeyPress={(event) =>
|
||||
event.key === KEYS.ENTER && handleClose()
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<p>
|
||||
<span role="img" aria-hidden="true" className="RoomDialog-emoji">
|
||||
{"🔒"}
|
||||
</span>
|
||||
{t("roomDialog.desc_privacy")}
|
||||
</p>
|
||||
<p>{t("roomDialog.desc_exitSession")}</p>
|
||||
<div className="RoomDialog-sessionStartButtonContainer">
|
||||
<DialogActionButton
|
||||
actionType="danger"
|
||||
label={t("roomDialog.button_stopSession")}
|
||||
onClick={() => {
|
||||
trackEvent("share", "room closed");
|
||||
onRoomDestroy();
|
||||
}}
|
||||
>
|
||||
{stop}
|
||||
</DialogActionButton>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Popover.Trigger>
|
||||
<Popover.Content
|
||||
onOpenAutoFocus={(event) => event.preventDefault()}
|
||||
onCloseAutoFocus={(event) => event.preventDefault()}
|
||||
className="RoomDialog__popover"
|
||||
side="top"
|
||||
align="end"
|
||||
sideOffset={5.5}
|
||||
>
|
||||
{tablerCheckIcon} copied
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
</div>
|
||||
<div className="RoomDialog__active__description">
|
||||
<p>
|
||||
<span
|
||||
role="img"
|
||||
aria-hidden="true"
|
||||
className="RoomDialog__active__description__emoji"
|
||||
>
|
||||
🔒{" "}
|
||||
</span>
|
||||
{t("roomDialog.desc_privacy")}
|
||||
</p>
|
||||
<p>{t("roomDialog.desc_exitSession")}</p>
|
||||
</div>
|
||||
|
||||
<div className="RoomDialog__active__actions">
|
||||
<FilledButton
|
||||
size="large"
|
||||
variant="outlined"
|
||||
color="danger"
|
||||
label={t("roomDialog.button_stopSession")}
|
||||
startIcon={playerStopFilledIcon}
|
||||
onClick={() => {
|
||||
trackEvent("share", "room closed");
|
||||
onRoomDestroy();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
small
|
||||
onCloseRequest={handleClose}
|
||||
title={t("labels.liveCollaboration")}
|
||||
theme={theme}
|
||||
>
|
||||
{renderRoomDialog()}
|
||||
<>
|
||||
<div className="RoomDialog__inactive__illustration">
|
||||
<CollabImage />
|
||||
</div>
|
||||
<div className="RoomDialog__inactive__header">
|
||||
{t("labels.liveCollaboration")}
|
||||
</div>
|
||||
|
||||
<div className="RoomDialog__inactive__description">
|
||||
<strong>{t("roomDialog.desc_intro")}</strong>
|
||||
{t("roomDialog.desc_privacy")}
|
||||
</div>
|
||||
|
||||
<div className="RoomDialog__inactive__start_session">
|
||||
<FilledButton
|
||||
size="large"
|
||||
label={t("roomDialog.button_startSession")}
|
||||
startIcon={playerPlayIcon}
|
||||
onClick={() => {
|
||||
trackEvent("share", "room creation", `ui (${getFrame()})`);
|
||||
onRoomCreate();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const RoomDialog = (props: RoomModalProps) => {
|
||||
return (
|
||||
<Dialog size="small" onCloseRequest={props.handleClose} title={false}>
|
||||
<div className="RoomDialog">
|
||||
<RoomModal {...props} />
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -673,8 +673,8 @@ const ExcalidrawWrapper = () => {
|
|||
{t("alerts.collabOfflineWarning")}
|
||||
</div>
|
||||
)}
|
||||
{excalidrawAPI && <Collab excalidrawAPI={excalidrawAPI} />}
|
||||
</Excalidraw>
|
||||
{excalidrawAPI && <Collab excalidrawAPI={excalidrawAPI} />}
|
||||
{errorMessage && (
|
||||
<ErrorDialog onClose={() => setErrorMessage("")}>
|
||||
{errorMessage}
|
||||
|
|
49
src/hooks/useCreatePortalContainer.ts
Normal file
49
src/hooks/useCreatePortalContainer.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
import { useState, useRef, useLayoutEffect } from "react";
|
||||
import { useDevice, useExcalidrawContainer } from "../components/App";
|
||||
import { useUIAppState } from "../context/ui-appState";
|
||||
|
||||
export const useCreatePortalContainer = (opts?: {
|
||||
className?: string;
|
||||
parentSelector?: string;
|
||||
}) => {
|
||||
const [div, setDiv] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
const device = useDevice();
|
||||
const { theme } = useUIAppState();
|
||||
const isMobileRef = useRef(device.isMobile);
|
||||
isMobileRef.current = device.isMobile;
|
||||
|
||||
const { container: excalidrawContainer } = useExcalidrawContainer();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (div) {
|
||||
div.classList.toggle("excalidraw--mobile", device.isMobile);
|
||||
}
|
||||
}, [div, device.isMobile]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const container = opts?.parentSelector
|
||||
? excalidrawContainer?.querySelector(opts.parentSelector)
|
||||
: document.body;
|
||||
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
const div = document.createElement("div");
|
||||
|
||||
div.classList.add("excalidraw", ...(opts?.className?.split(/\s+/) || []));
|
||||
div.classList.toggle("excalidraw--mobile", isMobileRef.current);
|
||||
div.classList.toggle("theme--dark", theme === "dark");
|
||||
|
||||
container.appendChild(div);
|
||||
|
||||
setDiv(div);
|
||||
|
||||
return () => {
|
||||
container.removeChild(div);
|
||||
};
|
||||
}, [excalidrawContainer, theme, opts?.className, opts?.parentSelector]);
|
||||
|
||||
return div;
|
||||
};
|
77
src/hooks/useLibraryItemSvg.ts
Normal file
77
src/hooks/useLibraryItemSvg.ts
Normal file
|
@ -0,0 +1,77 @@
|
|||
import { atom, useAtom } from "jotai";
|
||||
import { useEffect, useState } from "react";
|
||||
import { COLOR_PALETTE } from "../colors";
|
||||
import { jotaiScope } from "../jotai";
|
||||
import { exportToSvg } from "../packages/utils";
|
||||
import { LibraryItem } from "../types";
|
||||
|
||||
export type SvgCache = Map<LibraryItem["id"], SVGSVGElement>;
|
||||
|
||||
export const libraryItemSvgsCache = atom<SvgCache>(new Map());
|
||||
|
||||
const exportLibraryItemToSvg = async (elements: LibraryItem["elements"]) => {
|
||||
return await exportToSvg({
|
||||
elements,
|
||||
appState: {
|
||||
exportBackground: false,
|
||||
viewBackgroundColor: COLOR_PALETTE.white,
|
||||
},
|
||||
files: null,
|
||||
});
|
||||
};
|
||||
|
||||
export const useLibraryItemSvg = (
|
||||
id: LibraryItem["id"] | null,
|
||||
elements: LibraryItem["elements"] | undefined,
|
||||
svgCache: SvgCache,
|
||||
): SVGSVGElement | undefined => {
|
||||
const [svg, setSvg] = useState<SVGSVGElement>();
|
||||
|
||||
useEffect(() => {
|
||||
if (elements) {
|
||||
if (id) {
|
||||
// Try to load cached svg
|
||||
const cachedSvg = svgCache.get(id);
|
||||
|
||||
if (cachedSvg) {
|
||||
setSvg(cachedSvg);
|
||||
} else {
|
||||
// When there is no svg in cache export it and save to cache
|
||||
(async () => {
|
||||
const exportedSvg = await exportLibraryItemToSvg(elements);
|
||||
exportedSvg.querySelector(".style-fonts")?.remove();
|
||||
|
||||
if (exportedSvg) {
|
||||
svgCache.set(id, exportedSvg);
|
||||
setSvg(exportedSvg);
|
||||
}
|
||||
})();
|
||||
}
|
||||
} else {
|
||||
// When we have no id (usualy selected items from canvas) just export the svg
|
||||
(async () => {
|
||||
const exportedSvg = await exportLibraryItemToSvg(elements);
|
||||
setSvg(exportedSvg);
|
||||
})();
|
||||
}
|
||||
}
|
||||
}, [id, elements, svgCache, setSvg]);
|
||||
|
||||
return svg;
|
||||
};
|
||||
|
||||
export const useLibraryCache = () => {
|
||||
const [svgCache] = useAtom(libraryItemSvgsCache, jotaiScope);
|
||||
|
||||
const clearLibraryCache = () => svgCache.clear();
|
||||
|
||||
const deleteItemsFromLibraryCache = (items: LibraryItem["id"][]) => {
|
||||
items.forEach((item) => svgCache.delete(item));
|
||||
};
|
||||
|
||||
return {
|
||||
clearLibraryCache,
|
||||
deleteItemsFromLibraryCache,
|
||||
svgCache,
|
||||
};
|
||||
};
|
|
@ -1,42 +1,86 @@
|
|||
import { useEffect, useRef } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { EVENT } from "../constants";
|
||||
|
||||
export const useOutsideClick = (handler: (event: Event) => void) => {
|
||||
const ref = useRef(null);
|
||||
export function useOutsideClick<T extends HTMLElement>(
|
||||
ref: React.RefObject<T>,
|
||||
/** if performance is of concern, memoize the callback */
|
||||
callback: (event: Event) => void,
|
||||
/**
|
||||
* Optional callback which is called on every click.
|
||||
*
|
||||
* Should return `true` if click should be considered as inside the container,
|
||||
* and `false` if it falls outside and should call the `callback`.
|
||||
*
|
||||
* Returning `true` overrides the default behavior and `callback` won't be
|
||||
* called.
|
||||
*
|
||||
* Returning `undefined` will fallback to the default behavior.
|
||||
*/
|
||||
isInside?: (
|
||||
event: Event & { target: HTMLElement },
|
||||
/** the element of the passed ref */
|
||||
container: T,
|
||||
) => boolean | undefined,
|
||||
) {
|
||||
useEffect(() => {
|
||||
function onOutsideClick(event: Event) {
|
||||
const _event = event as Event & { target: T };
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
const listener = (event: Event) => {
|
||||
const current = ref.current as HTMLElement | null;
|
||||
if (!ref.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Do nothing if clicking ref's element or descendent elements
|
||||
if (
|
||||
!current ||
|
||||
current.contains(event.target as Node) ||
|
||||
[...document.querySelectorAll("[data-prevent-outside-click]")].some(
|
||||
(el) => el.contains(event.target as Node),
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const isInsideOverride = isInside?.(_event, ref.current);
|
||||
|
||||
handler(event);
|
||||
};
|
||||
if (isInsideOverride === true) {
|
||||
return;
|
||||
} else if (isInsideOverride === false) {
|
||||
return callback(_event);
|
||||
}
|
||||
|
||||
document.addEventListener("pointerdown", listener);
|
||||
document.addEventListener("touchstart", listener);
|
||||
return () => {
|
||||
document.removeEventListener("pointerdown", listener);
|
||||
document.removeEventListener("touchstart", listener);
|
||||
};
|
||||
},
|
||||
// Add ref and handler to effect dependencies
|
||||
// It's worth noting that because passed in handler is a new ...
|
||||
// ... function on every render that will cause this effect ...
|
||||
// ... callback/cleanup to run every render. It's not a big deal ...
|
||||
// ... but to optimize you can wrap handler in useCallback before ...
|
||||
// ... passing it into this hook.
|
||||
[ref, handler],
|
||||
);
|
||||
// clicked element is in the descenendant of the target container
|
||||
if (
|
||||
ref.current.contains(_event.target) ||
|
||||
// target is detached from DOM (happens when the element is removed
|
||||
// on a pointerup event fired *before* this handler's pointerup is
|
||||
// dispatched)
|
||||
!document.documentElement.contains(_event.target)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
return ref;
|
||||
};
|
||||
const isClickOnRadixPortal =
|
||||
_event.target.closest("[data-radix-portal]") ||
|
||||
// when radix popup is in "modal" mode, it disables pointer events on
|
||||
// the `body` element, so the target element is going to be the `html`
|
||||
// (note: this won't work if we selectively re-enable pointer events on
|
||||
// specific elements as we do with navbar or excalidraw UI elements)
|
||||
(_event.target === document.documentElement &&
|
||||
document.body.style.pointerEvents === "none");
|
||||
|
||||
// if clicking on radix portal, assume it's a popup that
|
||||
// should be considered as part of the UI. Obviously this is a terrible
|
||||
// hack you can end up click on radix popups that outside the tree,
|
||||
// but it works for most cases and the downside is minimal for now
|
||||
if (isClickOnRadixPortal) {
|
||||
return;
|
||||
}
|
||||
|
||||
// clicking on a container that ignores outside clicks
|
||||
if (_event.target.closest("[data-prevent-outside-click]")) {
|
||||
return;
|
||||
}
|
||||
|
||||
callback(_event);
|
||||
}
|
||||
|
||||
// note: don't use `click` because it often reports incorrect `event.target`
|
||||
document.addEventListener(EVENT.POINTER_DOWN, onOutsideClick);
|
||||
document.addEventListener(EVENT.TOUCH_START, onOutsideClick);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener(EVENT.POINTER_DOWN, onOutsideClick);
|
||||
document.removeEventListener(EVENT.TOUCH_START, onOutsideClick);
|
||||
};
|
||||
}, [ref, callback, isInside]);
|
||||
}
|
||||
|
|
32
src/hooks/useScrollPosition.ts
Normal file
32
src/hooks/useScrollPosition.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import { useEffect } from "react";
|
||||
import { atom, useAtom } from "jotai";
|
||||
import throttle from "lodash.throttle";
|
||||
|
||||
const scrollPositionAtom = atom<number>(0);
|
||||
|
||||
export const useScrollPosition = <T extends HTMLElement>(
|
||||
elementRef: React.RefObject<T>,
|
||||
) => {
|
||||
const [scrollPosition, setScrollPosition] = useAtom(scrollPositionAtom);
|
||||
|
||||
useEffect(() => {
|
||||
const { current: element } = elementRef;
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleScroll = throttle(() => {
|
||||
const { scrollTop } = element;
|
||||
setScrollPosition(scrollTop);
|
||||
}, 200);
|
||||
|
||||
element.addEventListener("scroll", handleScroll);
|
||||
|
||||
return () => {
|
||||
handleScroll.cancel();
|
||||
element.removeEventListener("scroll", handleScroll);
|
||||
};
|
||||
}, [elementRef, setScrollPosition]);
|
||||
|
||||
return scrollPosition;
|
||||
};
|
9
src/hooks/useTransition.ts
Normal file
9
src/hooks/useTransition.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import React, { useCallback } from "react";
|
||||
|
||||
/** noop polyfill for v17. Subset of API available */
|
||||
function useTransitionPolyfill() {
|
||||
const startTransition = useCallback((callback: () => void) => callback(), []);
|
||||
return [false, startTransition] as const;
|
||||
}
|
||||
|
||||
export const useTransition = React.useTransition || useTransitionPolyfill;
|
|
@ -40,10 +40,6 @@
|
|||
"arrowhead_triangle": "مثلث",
|
||||
"fontSize": "حجم الخط",
|
||||
"fontFamily": "نوع الخط",
|
||||
"onlySelected": "المحدد فقط",
|
||||
"withBackground": "الخلفية",
|
||||
"exportEmbedScene": "تضمين المشهد",
|
||||
"exportEmbedScene_details": "سيتم حفظ بيانات المشهد في ملف PNG/SVG المصدّر بحيث يمكن استعادة المشهد منه.\nسيزيد حجم الملف المصدر.",
|
||||
"addWatermark": "إضافة \"مصنوعة بواسطة Excalidraw\"",
|
||||
"handDrawn": "رسم باليد",
|
||||
"normal": "عادي",
|
||||
|
@ -100,7 +96,6 @@
|
|||
"flipHorizontal": "قلب عامودي",
|
||||
"flipVertical": "قلب أفقي",
|
||||
"viewMode": "نمط العرض",
|
||||
"toggleExportColorScheme": "تبديل نظام ألوان الصادرات",
|
||||
"share": "مشاركة",
|
||||
"showStroke": "إظهار منتقي لون الخط",
|
||||
"showBackground": "إظهار منتقي لون الخلفية",
|
||||
|
@ -128,7 +123,8 @@
|
|||
"unlockAll": "فتح الكل"
|
||||
},
|
||||
"statusPublished": "نُشر",
|
||||
"sidebarLock": "إبقاء الشريط الجانبي مفتوح"
|
||||
"sidebarLock": "إبقاء الشريط الجانبي مفتوح",
|
||||
"eyeDropper": ""
|
||||
},
|
||||
"library": {
|
||||
"noItems": "لا توجد عناصر أضيفت بعد...",
|
||||
|
@ -140,11 +136,7 @@
|
|||
"exportJSON": "صدر الملف",
|
||||
"exportImage": "تصدير الصورة...",
|
||||
"export": "حفظ إلى...",
|
||||
"exportToPng": "تصدير بصيغة PNG",
|
||||
"exportToSvg": "تصدير بصيغة SVG",
|
||||
"copyToClipboard": "نسخ إلى الحافظة",
|
||||
"copyPngToClipboard": "نسخ الـ PNG إلى الحافظة",
|
||||
"scale": "مقاس",
|
||||
"save": "احفظ للملف الحالي",
|
||||
"saveAs": "حفظ كـ",
|
||||
"load": "فتح",
|
||||
|
@ -363,6 +355,30 @@
|
|||
"resetLibrary": "إعادة ضبط المكتبة",
|
||||
"removeItemsFromLib": "إزالة العناصر المحددة من المكتبة"
|
||||
},
|
||||
"imageExportDialog": {
|
||||
"header": "",
|
||||
"label": {
|
||||
"withBackground": "",
|
||||
"onlySelected": "",
|
||||
"darkMode": "",
|
||||
"embedScene": "",
|
||||
"scale": "",
|
||||
"padding": ""
|
||||
},
|
||||
"tooltip": {
|
||||
"embedScene": ""
|
||||
},
|
||||
"title": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
},
|
||||
"button": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
}
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "رسوماتك مشفرة من النهاية إلى النهاية حتى أن خوادم Excalidraw لن تراها أبدا.",
|
||||
"link": "مشاركة المدونة في التشفير من النهاية إلى النهاية في Excalidraw"
|
||||
|
|
|
@ -40,10 +40,6 @@
|
|||
"arrowhead_triangle": "Триъгълник",
|
||||
"fontSize": "Размер на шрифта",
|
||||
"fontFamily": "Семейство шрифтове",
|
||||
"onlySelected": "Само избраното",
|
||||
"withBackground": "Фон",
|
||||
"exportEmbedScene": "",
|
||||
"exportEmbedScene_details": "Данните от сцената ще бъдат екпортирани в PNG/SVG файл, за да може сцената да бъде възстановена от него.\nТова ще увеличи размера на файла.",
|
||||
"addWatermark": "Добави \"Направено с Excalidraw\"",
|
||||
"handDrawn": "Нарисувано на ръка",
|
||||
"normal": "Нормален",
|
||||
|
@ -100,7 +96,6 @@
|
|||
"flipHorizontal": "Хоризонтално обръщане",
|
||||
"flipVertical": "Вертикално обръщане",
|
||||
"viewMode": "Изглед",
|
||||
"toggleExportColorScheme": "",
|
||||
"share": "Сподели",
|
||||
"showStroke": "",
|
||||
"showBackground": "",
|
||||
|
@ -128,7 +123,8 @@
|
|||
"unlockAll": ""
|
||||
},
|
||||
"statusPublished": "",
|
||||
"sidebarLock": ""
|
||||
"sidebarLock": "",
|
||||
"eyeDropper": ""
|
||||
},
|
||||
"library": {
|
||||
"noItems": "",
|
||||
|
@ -140,11 +136,7 @@
|
|||
"exportJSON": "",
|
||||
"exportImage": "",
|
||||
"export": "",
|
||||
"exportToPng": "Изнасяне в PNG",
|
||||
"exportToSvg": "Изнасяне в SVG",
|
||||
"copyToClipboard": "Копиране в клипборда",
|
||||
"copyPngToClipboard": "Копирай PNG в клипборда",
|
||||
"scale": "Мащаб",
|
||||
"save": "",
|
||||
"saveAs": "Запиши като",
|
||||
"load": "",
|
||||
|
@ -363,6 +355,30 @@
|
|||
"resetLibrary": "",
|
||||
"removeItemsFromLib": ""
|
||||
},
|
||||
"imageExportDialog": {
|
||||
"header": "",
|
||||
"label": {
|
||||
"withBackground": "",
|
||||
"onlySelected": "",
|
||||
"darkMode": "",
|
||||
"embedScene": "",
|
||||
"scale": "",
|
||||
"padding": ""
|
||||
},
|
||||
"tooltip": {
|
||||
"embedScene": ""
|
||||
},
|
||||
"title": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
},
|
||||
"button": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
}
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "Вашите рисунки са криптирани от край до край, така че сървърите на Excalidraw няма да могат да ги виждат.",
|
||||
"link": ""
|
||||
|
|
|
@ -40,10 +40,6 @@
|
|||
"arrowhead_triangle": "ত্রিভূজ",
|
||||
"fontSize": "লেখনীর মাত্রা",
|
||||
"fontFamily": "লেখনীর হরফ",
|
||||
"onlySelected": "শুধুমাত্র সিলেক্টকৃত",
|
||||
"withBackground": "পটভূমি সমেত",
|
||||
"exportEmbedScene": "দৃশ্য",
|
||||
"exportEmbedScene_details": "সিনের ডেটা এক্সপোর্টকৃত পীএনজী বা এসভীজী ফাইলের মধ্যে সেভ করা হবে যাতে করে পরবর্তী সময়ে আপনি এডিট করতে পারেন। তবে এতে ফাইলের সাইজ বাড়বে",
|
||||
"addWatermark": "এক্সক্যালিড্র দ্বারা প্রস্তুত",
|
||||
"handDrawn": "হাতে আঁকা",
|
||||
"normal": "স্বাভাবিক",
|
||||
|
@ -100,7 +96,6 @@
|
|||
"flipHorizontal": "অনুভূমিক আবর্তন",
|
||||
"flipVertical": "উলম্ব আবর্তন",
|
||||
"viewMode": "দৃশ্য",
|
||||
"toggleExportColorScheme": "",
|
||||
"share": "ভাগ করুন",
|
||||
"showStroke": "",
|
||||
"showBackground": "",
|
||||
|
@ -128,7 +123,8 @@
|
|||
"unlockAll": "সব বিচ্ছিন্ন করুন"
|
||||
},
|
||||
"statusPublished": "প্রকাশিত",
|
||||
"sidebarLock": "লক"
|
||||
"sidebarLock": "লক",
|
||||
"eyeDropper": ""
|
||||
},
|
||||
"library": {
|
||||
"noItems": "সংগ্রহে কিছু যোগ করা হয়নি",
|
||||
|
@ -140,11 +136,7 @@
|
|||
"exportJSON": "জেসন নিবদ্ধ করুন",
|
||||
"exportImage": "",
|
||||
"export": "",
|
||||
"exportToPng": "পীএনজী ছবির মতন নিবদ্ধ করুন",
|
||||
"exportToSvg": "এসভীজী ছবির মতন নিবদ্ধ করুন",
|
||||
"copyToClipboard": "ক্লিপবোর্ডে কপি করুন",
|
||||
"copyPngToClipboard": "পীএনজী ছবির মতন ক্লিপবোর্ডে কপি করুন",
|
||||
"scale": "মাপ",
|
||||
"save": "জমা করুন",
|
||||
"saveAs": "অন্যভাবে জমা করুন",
|
||||
"load": "",
|
||||
|
@ -363,6 +355,30 @@
|
|||
"resetLibrary": "",
|
||||
"removeItemsFromLib": ""
|
||||
},
|
||||
"imageExportDialog": {
|
||||
"header": "",
|
||||
"label": {
|
||||
"withBackground": "",
|
||||
"onlySelected": "",
|
||||
"darkMode": "",
|
||||
"embedScene": "",
|
||||
"scale": "",
|
||||
"padding": ""
|
||||
},
|
||||
"tooltip": {
|
||||
"embedScene": ""
|
||||
},
|
||||
"title": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
},
|
||||
"button": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
}
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "",
|
||||
"link": ""
|
||||
|
|
|
@ -40,10 +40,6 @@
|
|||
"arrowhead_triangle": "Triangle",
|
||||
"fontSize": "Mida de lletra",
|
||||
"fontFamily": "Tipus de lletra",
|
||||
"onlySelected": "Només seleccionats",
|
||||
"withBackground": "Fons",
|
||||
"exportEmbedScene": "Insereix l'escena",
|
||||
"exportEmbedScene_details": "Les dades de l’escena es desaran al fitxer PNG/SVG de manera que es pugui restaurar l’escena.\nAugmentarà la mida del fitxer exportat.",
|
||||
"addWatermark": "Afegeix-hi «Fet amb Excalidraw»",
|
||||
"handDrawn": "Dibuixat a mà",
|
||||
"normal": "Normal",
|
||||
|
@ -100,7 +96,6 @@
|
|||
"flipHorizontal": "Capgira horitzontalment",
|
||||
"flipVertical": "Capgira verticalment",
|
||||
"viewMode": "Mode de visualització",
|
||||
"toggleExportColorScheme": "Canvia l'esquema de colors de l'exportació",
|
||||
"share": "Comparteix",
|
||||
"showStroke": "Mostra el selector de color del traç",
|
||||
"showBackground": "Mostra el selector de color de fons",
|
||||
|
@ -128,7 +123,8 @@
|
|||
"unlockAll": "Desbloca-ho tot"
|
||||
},
|
||||
"statusPublished": "Publicat",
|
||||
"sidebarLock": "Manté la barra lateral oberta"
|
||||
"sidebarLock": "Manté la barra lateral oberta",
|
||||
"eyeDropper": ""
|
||||
},
|
||||
"library": {
|
||||
"noItems": "Encara no s'hi han afegit elements...",
|
||||
|
@ -140,11 +136,7 @@
|
|||
"exportJSON": "Exporta a un fitxer",
|
||||
"exportImage": "Exporta la imatge...",
|
||||
"export": "Guardar a...",
|
||||
"exportToPng": "Exporta a PNG",
|
||||
"exportToSvg": "Exporta a SNG",
|
||||
"copyToClipboard": "Copia al porta-retalls",
|
||||
"copyPngToClipboard": "Copia el PNG al porta-retalls",
|
||||
"scale": "Escala",
|
||||
"save": "Desa al fitxer actual",
|
||||
"saveAs": "Anomena i desa",
|
||||
"load": "Obrir",
|
||||
|
@ -363,6 +355,30 @@
|
|||
"resetLibrary": "Restableix la biblioteca",
|
||||
"removeItemsFromLib": "Suprimeix els elements seleccionats de la llibreria"
|
||||
},
|
||||
"imageExportDialog": {
|
||||
"header": "",
|
||||
"label": {
|
||||
"withBackground": "",
|
||||
"onlySelected": "",
|
||||
"darkMode": "",
|
||||
"embedScene": "",
|
||||
"scale": "",
|
||||
"padding": ""
|
||||
},
|
||||
"tooltip": {
|
||||
"embedScene": ""
|
||||
},
|
||||
"title": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
},
|
||||
"button": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
}
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "Els vostres dibuixos estan xifrats de punta a punta de manera que els servidors d’Excalidraw no els veuran mai.",
|
||||
"link": "Article del blog sobre encriptació d'extrem a extrem a Excalidraw"
|
||||
|
|
|
@ -40,10 +40,6 @@
|
|||
"arrowhead_triangle": "Trojúhelník",
|
||||
"fontSize": "Velikost písma",
|
||||
"fontFamily": "Písmo",
|
||||
"onlySelected": "Pouze vybrané",
|
||||
"withBackground": "Pozadí",
|
||||
"exportEmbedScene": "Vložit scénu",
|
||||
"exportEmbedScene_details": "Data scény budou uložena do exportovaného souboru PNG/SVG tak, aby z něj mohla být scéna obnovena.\nZvýší se velikost exportovaného souboru.",
|
||||
"addWatermark": "Přidat \"Vyrobeno s Excalidraw\"",
|
||||
"handDrawn": "Od ruky",
|
||||
"normal": "Normální",
|
||||
|
@ -100,7 +96,6 @@
|
|||
"flipHorizontal": "Převrátit vodorovně",
|
||||
"flipVertical": "Převrátit svisle",
|
||||
"viewMode": "Náhled",
|
||||
"toggleExportColorScheme": "Přepnout exportování barevného schématu",
|
||||
"share": "Sdílet",
|
||||
"showStroke": "Zobrazit výběr barvy",
|
||||
"showBackground": "Zobrazit výběr barev pozadí",
|
||||
|
@ -128,7 +123,8 @@
|
|||
"unlockAll": "Odemknout vše"
|
||||
},
|
||||
"statusPublished": "Zveřejněno",
|
||||
"sidebarLock": "Ponechat postranní panel otevřený"
|
||||
"sidebarLock": "Ponechat postranní panel otevřený",
|
||||
"eyeDropper": ""
|
||||
},
|
||||
"library": {
|
||||
"noItems": "Dosud neexistují žádné položky...",
|
||||
|
@ -140,11 +136,7 @@
|
|||
"exportJSON": "Exportovat do souboru",
|
||||
"exportImage": "Exportovat obrázek...",
|
||||
"export": "Uložit jako...",
|
||||
"exportToPng": "Exportovat do PNG",
|
||||
"exportToSvg": "Exportovat do SVG",
|
||||
"copyToClipboard": "Kopírovat do schránky",
|
||||
"copyPngToClipboard": "Kopírovat PNG do schránky",
|
||||
"scale": "Měřítko",
|
||||
"save": "Uložit do aktuálního souboru",
|
||||
"saveAs": "Uložit jako",
|
||||
"load": "Otevřít",
|
||||
|
@ -363,6 +355,30 @@
|
|||
"resetLibrary": "",
|
||||
"removeItemsFromLib": ""
|
||||
},
|
||||
"imageExportDialog": {
|
||||
"header": "",
|
||||
"label": {
|
||||
"withBackground": "",
|
||||
"onlySelected": "",
|
||||
"darkMode": "",
|
||||
"embedScene": "",
|
||||
"scale": "",
|
||||
"padding": ""
|
||||
},
|
||||
"tooltip": {
|
||||
"embedScene": ""
|
||||
},
|
||||
"title": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
},
|
||||
"button": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
}
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "",
|
||||
"link": ""
|
||||
|
|
|
@ -40,10 +40,6 @@
|
|||
"arrowhead_triangle": "Trekant",
|
||||
"fontSize": "Skriftstørrelse",
|
||||
"fontFamily": "Skrifttypefamilie",
|
||||
"onlySelected": "Kun valgte",
|
||||
"withBackground": "Baggrund",
|
||||
"exportEmbedScene": "Indlejr scene",
|
||||
"exportEmbedScene_details": "Scene data vil blive gemt i den eksporterede PNG/SVG-fil, så scenen kan gendannes fra den.\nDette vil øge den eksporterede filstørrelse.",
|
||||
"addWatermark": "Tilføj \"Lavet med Excalidraw\"",
|
||||
"handDrawn": "Hånd-tegnet",
|
||||
"normal": "Normal",
|
||||
|
@ -100,7 +96,6 @@
|
|||
"flipHorizontal": "",
|
||||
"flipVertical": "",
|
||||
"viewMode": "",
|
||||
"toggleExportColorScheme": "",
|
||||
"share": "Del",
|
||||
"showStroke": "",
|
||||
"showBackground": "",
|
||||
|
@ -128,7 +123,8 @@
|
|||
"unlockAll": ""
|
||||
},
|
||||
"statusPublished": "",
|
||||
"sidebarLock": ""
|
||||
"sidebarLock": "",
|
||||
"eyeDropper": ""
|
||||
},
|
||||
"library": {
|
||||
"noItems": "",
|
||||
|
@ -140,11 +136,7 @@
|
|||
"exportJSON": "",
|
||||
"exportImage": "",
|
||||
"export": "",
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyToClipboard": "Kopier til klippebord",
|
||||
"copyPngToClipboard": "Kopier PNG til klippebord",
|
||||
"scale": "",
|
||||
"save": "",
|
||||
"saveAs": "Gem som",
|
||||
"load": "",
|
||||
|
@ -363,6 +355,30 @@
|
|||
"resetLibrary": "",
|
||||
"removeItemsFromLib": ""
|
||||
},
|
||||
"imageExportDialog": {
|
||||
"header": "",
|
||||
"label": {
|
||||
"withBackground": "",
|
||||
"onlySelected": "",
|
||||
"darkMode": "",
|
||||
"embedScene": "",
|
||||
"scale": "",
|
||||
"padding": ""
|
||||
},
|
||||
"tooltip": {
|
||||
"embedScene": ""
|
||||
},
|
||||
"title": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
},
|
||||
"button": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
}
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "",
|
||||
"link": ""
|
||||
|
|
|
@ -40,10 +40,6 @@
|
|||
"arrowhead_triangle": "Dreieck",
|
||||
"fontSize": "Schriftgröße",
|
||||
"fontFamily": "Schriftfamilie",
|
||||
"onlySelected": "Nur ausgewählte",
|
||||
"withBackground": "Hintergrund",
|
||||
"exportEmbedScene": "Szene einbetten",
|
||||
"exportEmbedScene_details": "Die Zeichnungsdaten werden in der exportierten PNG/SVG-Datei gespeichert, sodass das Dokument später weiter bearbeitet werden kann. \nDieses wird die exportierte Datei vergrößern.",
|
||||
"addWatermark": "\"Made with Excalidraw\" hinzufügen",
|
||||
"handDrawn": "Handgezeichnet",
|
||||
"normal": "Normal",
|
||||
|
@ -100,7 +96,6 @@
|
|||
"flipHorizontal": "Horizontal spiegeln",
|
||||
"flipVertical": "Vertikal spiegeln",
|
||||
"viewMode": "Ansichtsmodus",
|
||||
"toggleExportColorScheme": "Farbschema für Export umschalten",
|
||||
"share": "Teilen",
|
||||
"showStroke": "Auswahl für Strichfarbe anzeigen",
|
||||
"showBackground": "Hintergrundfarbe auswählen",
|
||||
|
@ -128,7 +123,8 @@
|
|||
"unlockAll": "Alle entsperren"
|
||||
},
|
||||
"statusPublished": "Veröffentlicht",
|
||||
"sidebarLock": "Seitenleiste offen lassen"
|
||||
"sidebarLock": "Seitenleiste offen lassen",
|
||||
"eyeDropper": ""
|
||||
},
|
||||
"library": {
|
||||
"noItems": "Noch keine Elemente hinzugefügt...",
|
||||
|
@ -140,11 +136,7 @@
|
|||
"exportJSON": "In Datei exportieren",
|
||||
"exportImage": "Exportiere Bild...",
|
||||
"export": "Speichern als...",
|
||||
"exportToPng": "Als PNG exportieren",
|
||||
"exportToSvg": "Als SVG exportieren",
|
||||
"copyToClipboard": "In Zwischenablage kopieren",
|
||||
"copyPngToClipboard": "PNG in die Zwischenablage kopieren",
|
||||
"scale": "Skalierung",
|
||||
"save": "In aktueller Datei speichern",
|
||||
"saveAs": "Speichern unter",
|
||||
"load": "Öffnen",
|
||||
|
@ -363,6 +355,30 @@
|
|||
"resetLibrary": "Bibliothek zurücksetzen",
|
||||
"removeItemsFromLib": "Ausgewählte Elemente aus der Bibliothek entfernen"
|
||||
},
|
||||
"imageExportDialog": {
|
||||
"header": "Bild exportieren",
|
||||
"label": {
|
||||
"withBackground": "Hintergrund",
|
||||
"onlySelected": "Nur ausgewählte",
|
||||
"darkMode": "Dunkler Modus",
|
||||
"embedScene": "Szene einbetten",
|
||||
"scale": "Skalierung",
|
||||
"padding": "Abstand"
|
||||
},
|
||||
"tooltip": {
|
||||
"embedScene": "Die Zeichnungsdaten werden in der exportierten PNG/SVG-Datei gespeichert, sodass das Dokument später weiter bearbeitet werden kann. \nDieses wird die exportierte Datei vergrößern."
|
||||
},
|
||||
"title": {
|
||||
"exportToPng": "Als PNG exportieren",
|
||||
"exportToSvg": "Als SVG exportieren",
|
||||
"copyPngToClipboard": "PNG in die Zwischenablage kopieren"
|
||||
},
|
||||
"button": {
|
||||
"exportToPng": "PNG",
|
||||
"exportToSvg": "SVG",
|
||||
"copyPngToClipboard": "In Zwischenablage kopieren"
|
||||
}
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "Da deine Zeichnungen Ende-zu-Ende verschlüsselt werden, sehen auch unsere Excalidraw-Server sie niemals.",
|
||||
"link": "Blogbeitrag über Ende-zu-Ende-Verschlüsselung in Excalidraw"
|
||||
|
@ -395,20 +411,20 @@
|
|||
},
|
||||
"colors": {
|
||||
"transparent": "Transparent",
|
||||
"black": "",
|
||||
"white": "",
|
||||
"red": "",
|
||||
"pink": "",
|
||||
"grape": "",
|
||||
"violet": "",
|
||||
"gray": "",
|
||||
"blue": "",
|
||||
"cyan": "",
|
||||
"teal": "",
|
||||
"green": "",
|
||||
"yellow": "",
|
||||
"orange": "",
|
||||
"bronze": ""
|
||||
"black": "Schwarz",
|
||||
"white": "Weiß",
|
||||
"red": "Rot",
|
||||
"pink": "Pink",
|
||||
"grape": "Traube",
|
||||
"violet": "Violett",
|
||||
"gray": "Grau",
|
||||
"blue": "Blau",
|
||||
"cyan": "Cyan",
|
||||
"teal": "Blaugrün",
|
||||
"green": "Grün",
|
||||
"yellow": "Gelb",
|
||||
"orange": "Orange",
|
||||
"bronze": "Bronze"
|
||||
},
|
||||
"welcomeScreen": {
|
||||
"app": {
|
||||
|
@ -424,10 +440,10 @@
|
|||
}
|
||||
},
|
||||
"colorPicker": {
|
||||
"mostUsedCustomColors": "",
|
||||
"colors": "",
|
||||
"shades": "",
|
||||
"hexCode": "",
|
||||
"noShades": ""
|
||||
"mostUsedCustomColors": "Beliebteste benutzerdefinierte Farben",
|
||||
"colors": "Farben",
|
||||
"shades": "Schattierungen",
|
||||
"hexCode": "Hex-Code",
|
||||
"noShades": "Keine Schattierungen für diese Farbe verfügbar"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,10 +40,6 @@
|
|||
"arrowhead_triangle": "Τρίγωνο",
|
||||
"fontSize": "Μέγεθος γραμματοσειράς",
|
||||
"fontFamily": "Γραμματοσειρά",
|
||||
"onlySelected": "Μόνο τα Επιλεγμένα",
|
||||
"withBackground": "Φόντο",
|
||||
"exportEmbedScene": "Ενσωμάτωση σκηνής",
|
||||
"exportEmbedScene_details": "Τα δεδομένα σκηνής θα αποθηκευτούν στο αρχείο PNG/SVG προς εξαγωγή ώστε η σκηνή να είναι δυνατό να αποκατασταθεί από αυτό.\nΘα αυξήσει το μέγεθος του αρχείου προς εξαγωγή.",
|
||||
"addWatermark": "Προσθήκη \"Φτιαγμένο με Excalidraw\"",
|
||||
"handDrawn": "Σχεδιασμένο στο χέρι",
|
||||
"normal": "Κανονική",
|
||||
|
@ -100,7 +96,6 @@
|
|||
"flipHorizontal": "Οριζόντια αναστροφή",
|
||||
"flipVertical": "Κατακόρυφη αναστροφή",
|
||||
"viewMode": "Λειτουργία προβολής",
|
||||
"toggleExportColorScheme": "Εναλλαγή εξαγωγής θέματος χρωμάτων",
|
||||
"share": "Κοινοποίηση",
|
||||
"showStroke": "Εμφάνιση επιλογέα χρωμάτων πινελιάς",
|
||||
"showBackground": "Εμφάνιση επιλογέα χρώματος φόντου",
|
||||
|
@ -128,7 +123,8 @@
|
|||
"unlockAll": "Ξεκλείδωμα όλων"
|
||||
},
|
||||
"statusPublished": "Δημοσιευμένο",
|
||||
"sidebarLock": "Κρατήστε την πλαϊνή μπάρα ανοιχτή"
|
||||
"sidebarLock": "Κρατήστε την πλαϊνή μπάρα ανοιχτή",
|
||||
"eyeDropper": ""
|
||||
},
|
||||
"library": {
|
||||
"noItems": "Δεν έχουν προστεθεί αντικείμενα ακόμη...",
|
||||
|
@ -140,11 +136,7 @@
|
|||
"exportJSON": "Εξαγωγή σε αρχείο",
|
||||
"exportImage": "Εξαγωγή εικόνας...",
|
||||
"export": "Αποθήκευση ως...",
|
||||
"exportToPng": "Εξαγωγή σε PNG",
|
||||
"exportToSvg": "Εξαγωγή σε SVG",
|
||||
"copyToClipboard": "Αντιγραφή στο πρόχειρο",
|
||||
"copyPngToClipboard": "Αντιγραφή PNG στο πρόχειρο",
|
||||
"scale": "Κλίμακα",
|
||||
"save": "Αποθήκευση στο τρέχον αρχείο",
|
||||
"saveAs": "Αποθήκευση ως",
|
||||
"load": "Άνοιγμα",
|
||||
|
@ -363,6 +355,30 @@
|
|||
"resetLibrary": "Καθαρισμός βιβλιοθήκης",
|
||||
"removeItemsFromLib": "Αφαίρεση επιλεγμένων αντικειμένων από τη βιβλιοθήκη"
|
||||
},
|
||||
"imageExportDialog": {
|
||||
"header": "",
|
||||
"label": {
|
||||
"withBackground": "",
|
||||
"onlySelected": "",
|
||||
"darkMode": "",
|
||||
"embedScene": "",
|
||||
"scale": "",
|
||||
"padding": ""
|
||||
},
|
||||
"tooltip": {
|
||||
"embedScene": ""
|
||||
},
|
||||
"title": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
},
|
||||
"button": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
}
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "Τα σχέδιά σου είναι κρυπτογραφημένα από άκρο σε άκρο, έτσι δεν θα είναι ποτέ ορατά μέσα από τους διακομιστές του Excalidraw.",
|
||||
"link": "Blog post στην κρυπτογράφηση end-to-end στο Excalidraw"
|
||||
|
@ -395,20 +411,20 @@
|
|||
},
|
||||
"colors": {
|
||||
"transparent": "Διαφανές",
|
||||
"black": "",
|
||||
"white": "",
|
||||
"red": "",
|
||||
"pink": "",
|
||||
"grape": "",
|
||||
"violet": "",
|
||||
"gray": "",
|
||||
"blue": "",
|
||||
"cyan": "",
|
||||
"teal": "",
|
||||
"green": "",
|
||||
"yellow": "",
|
||||
"orange": "",
|
||||
"bronze": ""
|
||||
"black": "Μαύρο",
|
||||
"white": "Λευκό",
|
||||
"red": "Κόκκινο",
|
||||
"pink": "Ροζ",
|
||||
"grape": "Σταφυλί",
|
||||
"violet": "Βιολετί",
|
||||
"gray": "Γκρι",
|
||||
"blue": "Μπλε",
|
||||
"cyan": "Κυανό",
|
||||
"teal": "Τιρκουάζ",
|
||||
"green": "Πράσινο",
|
||||
"yellow": "Κίτρινο",
|
||||
"orange": "Πορτοκαλί",
|
||||
"bronze": "Χαλκινο"
|
||||
},
|
||||
"welcomeScreen": {
|
||||
"app": {
|
||||
|
@ -424,10 +440,10 @@
|
|||
}
|
||||
},
|
||||
"colorPicker": {
|
||||
"mostUsedCustomColors": "",
|
||||
"colors": "",
|
||||
"shades": "",
|
||||
"hexCode": "",
|
||||
"noShades": ""
|
||||
"mostUsedCustomColors": "Πιο χρησιμοποιούμενα χρώματα",
|
||||
"colors": "Χρώματα",
|
||||
"shades": "Αποχρώσεις",
|
||||
"hexCode": "Κωδικός Hex",
|
||||
"noShades": "Δεν υπάρχουν διαθέσιμες αποχρώσεις για αυτό το χρώμα"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,10 +40,6 @@
|
|||
"arrowhead_triangle": "Triangle",
|
||||
"fontSize": "Font size",
|
||||
"fontFamily": "Font family",
|
||||
"onlySelected": "Only selected",
|
||||
"withBackground": "Background",
|
||||
"exportEmbedScene": "Embed scene",
|
||||
"exportEmbedScene_details": "Scene data will be saved into the exported PNG/SVG file so that the scene can be restored from it.\nWill increase exported file size.",
|
||||
"addWatermark": "Add \"Made with Excalidraw\"",
|
||||
"handDrawn": "Hand-drawn",
|
||||
"normal": "Normal",
|
||||
|
@ -100,7 +96,6 @@
|
|||
"flipHorizontal": "Flip horizontal",
|
||||
"flipVertical": "Flip vertical",
|
||||
"viewMode": "View mode",
|
||||
"toggleExportColorScheme": "Toggle export color scheme",
|
||||
"share": "Share",
|
||||
"showStroke": "Show stroke color picker",
|
||||
"showBackground": "Show background color picker",
|
||||
|
@ -128,7 +123,8 @@
|
|||
"unlockAll": "Unlock all"
|
||||
},
|
||||
"statusPublished": "Published",
|
||||
"sidebarLock": "Keep sidebar open"
|
||||
"sidebarLock": "Keep sidebar open",
|
||||
"eyeDropper": "Pick color from canvas"
|
||||
},
|
||||
"library": {
|
||||
"noItems": "No items added yet...",
|
||||
|
@ -140,11 +136,7 @@
|
|||
"exportJSON": "Export to file",
|
||||
"exportImage": "Export image...",
|
||||
"export": "Save to...",
|
||||
"exportToPng": "Export to PNG",
|
||||
"exportToSvg": "Export to SVG",
|
||||
"copyToClipboard": "Copy to clipboard",
|
||||
"copyPngToClipboard": "Copy PNG to clipboard",
|
||||
"scale": "Scale",
|
||||
"save": "Save to current file",
|
||||
"saveAs": "Save as",
|
||||
"load": "Open",
|
||||
|
@ -363,6 +355,30 @@
|
|||
"resetLibrary": "Reset library",
|
||||
"removeItemsFromLib": "Remove selected items from library"
|
||||
},
|
||||
"imageExportDialog": {
|
||||
"header": "Export image",
|
||||
"label": {
|
||||
"withBackground": "Background",
|
||||
"onlySelected": "Only selected",
|
||||
"darkMode": "Dark mode",
|
||||
"embedScene": "Embed scene",
|
||||
"scale": "Scale",
|
||||
"padding": "Padding"
|
||||
},
|
||||
"tooltip": {
|
||||
"embedScene": "Scene data will be saved into the exported PNG/SVG file so that the scene can be restored from it.\nWill increase exported file size."
|
||||
},
|
||||
"title": {
|
||||
"exportToPng": "Export to PNG",
|
||||
"exportToSvg": "Export to SVG",
|
||||
"copyPngToClipboard": "Copy PNG to clipboard"
|
||||
},
|
||||
"button": {
|
||||
"exportToPng": "PNG",
|
||||
"exportToSvg": "SVG",
|
||||
"copyPngToClipboard": "Copy to clipboard"
|
||||
}
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "Your drawings are end-to-end encrypted so Excalidraw's servers will never see them.",
|
||||
"link": "Blog post on end-to-end encryption in Excalidraw"
|
||||
|
|
|
@ -40,10 +40,6 @@
|
|||
"arrowhead_triangle": "Triángulo",
|
||||
"fontSize": "Tamaño de la fuente",
|
||||
"fontFamily": "Tipo de fuente",
|
||||
"onlySelected": "Sólo seleccionados",
|
||||
"withBackground": "Fondo",
|
||||
"exportEmbedScene": "Incrustar escena",
|
||||
"exportEmbedScene_details": "Los datos de escena se guardarán en el archivo PNG/SVG exportado, así la escena puede ser restaurada de la misma.\nEsto aumentará el tamaño del archivo exportado.",
|
||||
"addWatermark": "Agregar \"Hecho con Excalidraw\"",
|
||||
"handDrawn": "Dibujado a mano",
|
||||
"normal": "Normal",
|
||||
|
@ -100,7 +96,6 @@
|
|||
"flipHorizontal": "Girar horizontalmente",
|
||||
"flipVertical": "Girar verticalmente",
|
||||
"viewMode": "Modo presentación",
|
||||
"toggleExportColorScheme": "Alternar el esquema de color de exportación",
|
||||
"share": "Compartir",
|
||||
"showStroke": "Mostrar selector de color de trazo",
|
||||
"showBackground": "Mostrar el selector de color de fondo",
|
||||
|
@ -128,7 +123,8 @@
|
|||
"unlockAll": "Desbloquear todo"
|
||||
},
|
||||
"statusPublished": "Publicado",
|
||||
"sidebarLock": "Mantener barra lateral abierta"
|
||||
"sidebarLock": "Mantener barra lateral abierta",
|
||||
"eyeDropper": ""
|
||||
},
|
||||
"library": {
|
||||
"noItems": "No hay elementos añadidos todavía...",
|
||||
|
@ -140,11 +136,7 @@
|
|||
"exportJSON": "Exportar a archivo",
|
||||
"exportImage": "Exportar imagen...",
|
||||
"export": "Guardar en...",
|
||||
"exportToPng": "Exportar a PNG",
|
||||
"exportToSvg": "Exportar a SVG",
|
||||
"copyToClipboard": "Copiar al portapapeles",
|
||||
"copyPngToClipboard": "Copiar PNG al portapapeles",
|
||||
"scale": "Escalar",
|
||||
"save": "Guardar en archivo actual",
|
||||
"saveAs": "Guardar como",
|
||||
"load": "Abrir",
|
||||
|
@ -363,6 +355,30 @@
|
|||
"resetLibrary": "Reiniciar biblioteca",
|
||||
"removeItemsFromLib": "Eliminar elementos seleccionados de la biblioteca"
|
||||
},
|
||||
"imageExportDialog": {
|
||||
"header": "",
|
||||
"label": {
|
||||
"withBackground": "",
|
||||
"onlySelected": "",
|
||||
"darkMode": "",
|
||||
"embedScene": "",
|
||||
"scale": "",
|
||||
"padding": ""
|
||||
},
|
||||
"tooltip": {
|
||||
"embedScene": ""
|
||||
},
|
||||
"title": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
},
|
||||
"button": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
}
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "Tus dibujos están cifrados de punto a punto, por lo que los servidores de Excalidraw nunca los verán.",
|
||||
"link": "Entrada en el blog sobre cifrado de extremo a extremo"
|
||||
|
|
|
@ -40,10 +40,6 @@
|
|||
"arrowhead_triangle": "Hirukia",
|
||||
"fontSize": "Letra-tamaina",
|
||||
"fontFamily": "Letra-tipoa",
|
||||
"onlySelected": "Hautapena soilik",
|
||||
"withBackground": "Atzeko planoa",
|
||||
"exportEmbedScene": "Txertatu eszena",
|
||||
"exportEmbedScene_details": "Eszenaren datuak esportatutako PNG/SVG fitxategian gordeko dira, eszena bertatik berrezartzeko.\nEsportatutako fitxategien tamaina handituko da.",
|
||||
"addWatermark": "Gehitu \"Excalidraw bidez egina\"",
|
||||
"handDrawn": "Eskuz marraztua",
|
||||
"normal": "Normala",
|
||||
|
@ -100,7 +96,6 @@
|
|||
"flipHorizontal": "Irauli horizontalki",
|
||||
"flipVertical": "Irauli bertikalki",
|
||||
"viewMode": "Ikuspegia",
|
||||
"toggleExportColorScheme": "Aldatu esportatzeko kolorearen eszena",
|
||||
"share": "Partekatu",
|
||||
"showStroke": "Erakutsi marraren kolore-hautatzailea",
|
||||
"showBackground": "Erakutsi atzeko planoaren kolore-hautatzailea",
|
||||
|
@ -128,7 +123,8 @@
|
|||
"unlockAll": "Desblokeatu guztiak"
|
||||
},
|
||||
"statusPublished": "Argitaratua",
|
||||
"sidebarLock": "Mantendu alboko barra irekita"
|
||||
"sidebarLock": "Mantendu alboko barra irekita",
|
||||
"eyeDropper": ""
|
||||
},
|
||||
"library": {
|
||||
"noItems": "Oraindik ez da elementurik gehitu...",
|
||||
|
@ -140,11 +136,7 @@
|
|||
"exportJSON": "Esportatu fitxategira",
|
||||
"exportImage": "Esportatu irudia...",
|
||||
"export": "Gorde hemen...",
|
||||
"exportToPng": "Esportatu PNG gisa",
|
||||
"exportToSvg": "Esportatu SVG gisa",
|
||||
"copyToClipboard": "Kopiatu arbelera",
|
||||
"copyPngToClipboard": "Kopiatu PNG arbelera",
|
||||
"scale": "Eskala",
|
||||
"save": "Gorde uneko fitxategian",
|
||||
"saveAs": "Gorde honela",
|
||||
"load": "Ireki",
|
||||
|
@ -363,6 +355,30 @@
|
|||
"resetLibrary": "Leheneratu liburutegia",
|
||||
"removeItemsFromLib": "Kendu hautatutako elementuak liburutegitik"
|
||||
},
|
||||
"imageExportDialog": {
|
||||
"header": "",
|
||||
"label": {
|
||||
"withBackground": "",
|
||||
"onlySelected": "",
|
||||
"darkMode": "",
|
||||
"embedScene": "",
|
||||
"scale": "",
|
||||
"padding": ""
|
||||
},
|
||||
"tooltip": {
|
||||
"embedScene": ""
|
||||
},
|
||||
"title": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
},
|
||||
"button": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
}
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "Zure marrazkiak muturretik muturrera enkriptatu dira, beraz Excalidraw-ren zerbitzariek ezingo dituzte ikusi.",
|
||||
"link": "Excalidraw-ren muturretik muturrerako enkriptatzearen gaineko mezua blogean"
|
||||
|
|
|
@ -40,10 +40,6 @@
|
|||
"arrowhead_triangle": "مثلث",
|
||||
"fontSize": "اندازه قلم",
|
||||
"fontFamily": "نوع قلم",
|
||||
"onlySelected": "فقط انتخاب شده ها",
|
||||
"withBackground": "پس زمینه",
|
||||
"exportEmbedScene": "تعبیه صحنه",
|
||||
"exportEmbedScene_details": "متحوای صحنه به فایل خروجی SVG/PNG اضافه خواهد شد برای بازیابی صحنه به آن اضافه خواهد شد.\nباعث افزایش حجم فایل خروجی میشود.",
|
||||
"addWatermark": "\"ساخته شده با Excalidraw\" را اضافه کن",
|
||||
"handDrawn": "دست نویس",
|
||||
"normal": "عادی",
|
||||
|
@ -100,7 +96,6 @@
|
|||
"flipHorizontal": "چرخش افقی",
|
||||
"flipVertical": "چرخش عمودی",
|
||||
"viewMode": "حالت نمایش",
|
||||
"toggleExportColorScheme": "تغییر طرح خروجی رنگ",
|
||||
"share": "اشتراکگذاری",
|
||||
"showStroke": "نمایش انتخاب کننده رنگ حاشیه",
|
||||
"showBackground": "نمایش انتخاب کننده رنگ پس زمینه",
|
||||
|
@ -128,7 +123,8 @@
|
|||
"unlockAll": "باز کردن قفل همه"
|
||||
},
|
||||
"statusPublished": "منتشر شده",
|
||||
"sidebarLock": "باز نگه داشتن سایدبار"
|
||||
"sidebarLock": "باز نگه داشتن سایدبار",
|
||||
"eyeDropper": ""
|
||||
},
|
||||
"library": {
|
||||
"noItems": "آیتمی به اینجا اضافه نشده...",
|
||||
|
@ -140,11 +136,7 @@
|
|||
"exportJSON": "خروجی در فایل",
|
||||
"exportImage": "خروجی گرفتن از تصویر...",
|
||||
"export": "ذخیره در...",
|
||||
"exportToPng": "تبدیل به PNG",
|
||||
"exportToSvg": "تبدیل به SVG",
|
||||
"copyToClipboard": "کپی در حافظه موقت",
|
||||
"copyPngToClipboard": "کپی PNG در حافظه موقت",
|
||||
"scale": "مقیاس",
|
||||
"save": "ذخیره در همین فایل",
|
||||
"saveAs": "ذخیره با نام",
|
||||
"load": "باز کردن",
|
||||
|
@ -363,6 +355,30 @@
|
|||
"resetLibrary": "تنظیم مجدد کتابخانه",
|
||||
"removeItemsFromLib": "موارد انتخاب شده از موارد پسندیده حذف شوند"
|
||||
},
|
||||
"imageExportDialog": {
|
||||
"header": "",
|
||||
"label": {
|
||||
"withBackground": "",
|
||||
"onlySelected": "",
|
||||
"darkMode": "",
|
||||
"embedScene": "",
|
||||
"scale": "",
|
||||
"padding": ""
|
||||
},
|
||||
"tooltip": {
|
||||
"embedScene": ""
|
||||
},
|
||||
"title": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
},
|
||||
"button": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
}
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "شما در یک محیط رمزگزاری شده دو طرفه در حال طراحی هستید پس Excalidraw هرگز طرح های شما را نمیبند.",
|
||||
"link": "پست وبلاگ در مورد رمزگذاری سرتاسر در Excalidraw"
|
||||
|
|
|
@ -40,10 +40,6 @@
|
|||
"arrowhead_triangle": "Kolmio",
|
||||
"fontSize": "Kirjasinkoko",
|
||||
"fontFamily": "Kirjasintyyppi",
|
||||
"onlySelected": "Vain valitut",
|
||||
"withBackground": "Taustalla",
|
||||
"exportEmbedScene": "Upota työ",
|
||||
"exportEmbedScene_details": "Teoksen tiedot tallennetaan PNG/SVG-tiedostoon, jolloin teoksen voi palauttaa siitä. Kasvattaa tallennetun tiedoston kokoa.",
|
||||
"addWatermark": "Lisää \"Tehty Excalidrawilla\"",
|
||||
"handDrawn": "Käsinpiirretty",
|
||||
"normal": "Tavallinen",
|
||||
|
@ -100,7 +96,6 @@
|
|||
"flipHorizontal": "Käännä vaakasuunnassa",
|
||||
"flipVertical": "Käännä pystysuunnassa",
|
||||
"viewMode": "Katselutila",
|
||||
"toggleExportColorScheme": "Vaihda viennin väriteema",
|
||||
"share": "Jaa",
|
||||
"showStroke": "Näytä viivan värin valitsin",
|
||||
"showBackground": "Näytä taustavärin valitsin",
|
||||
|
@ -128,7 +123,8 @@
|
|||
"unlockAll": "Poista lukitus kaikista"
|
||||
},
|
||||
"statusPublished": "Julkaistu",
|
||||
"sidebarLock": "Pidä sivupalkki avoinna"
|
||||
"sidebarLock": "Pidä sivupalkki avoinna",
|
||||
"eyeDropper": ""
|
||||
},
|
||||
"library": {
|
||||
"noItems": "Kirjastossa ei ole vielä yhtään kohdetta...",
|
||||
|
@ -140,11 +136,7 @@
|
|||
"exportJSON": "Vie tiedostoon",
|
||||
"exportImage": "Vie kuva...",
|
||||
"export": "Tallenna nimellä...",
|
||||
"exportToPng": "Vie PNG-tiedostona",
|
||||
"exportToSvg": "Vie SVG-tiedostona",
|
||||
"copyToClipboard": "Kopioi leikepöydälle",
|
||||
"copyPngToClipboard": "Kopioi PNG-tiedosto leikepöydälle",
|
||||
"scale": "Koko",
|
||||
"save": "Tallenna nykyiseen tiedostoon",
|
||||
"saveAs": "Tallenna nimellä",
|
||||
"load": "Avaa",
|
||||
|
@ -363,6 +355,30 @@
|
|||
"resetLibrary": "Tyhjennä kirjasto",
|
||||
"removeItemsFromLib": "Poista valitut kohteet kirjastosta"
|
||||
},
|
||||
"imageExportDialog": {
|
||||
"header": "",
|
||||
"label": {
|
||||
"withBackground": "",
|
||||
"onlySelected": "",
|
||||
"darkMode": "",
|
||||
"embedScene": "",
|
||||
"scale": "",
|
||||
"padding": ""
|
||||
},
|
||||
"tooltip": {
|
||||
"embedScene": ""
|
||||
},
|
||||
"title": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
},
|
||||
"button": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
}
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "Piirroksesi ovat päästä-päähän-salattuja, joten Excalidrawin palvelimet eivät koskaan näe niitä.",
|
||||
"link": "Blogiartikkeli päästä päähän -salauksesta Excalidraw:ssa"
|
||||
|
|
|
@ -40,10 +40,6 @@
|
|||
"arrowhead_triangle": "Triangle",
|
||||
"fontSize": "Taille de la police",
|
||||
"fontFamily": "Police",
|
||||
"onlySelected": "Uniquement la sélection",
|
||||
"withBackground": "Arrière-plan",
|
||||
"exportEmbedScene": "Intégrer la scène",
|
||||
"exportEmbedScene_details": "Les données de scène seront enregistrées dans le fichier PNG/SVG exporté, afin que la scène puisse être restaurée à partir de celui-ci.\nCela augmentera la taille du fichier exporté.",
|
||||
"addWatermark": "Ajouter \"Réalisé avec Excalidraw\"",
|
||||
"handDrawn": "À main levée",
|
||||
"normal": "Normale",
|
||||
|
@ -100,7 +96,6 @@
|
|||
"flipHorizontal": "Retourner horizontalement",
|
||||
"flipVertical": "Retourner verticalement",
|
||||
"viewMode": "Mode présentation",
|
||||
"toggleExportColorScheme": "Activer/Désactiver l'export du thème de couleur",
|
||||
"share": "Partager",
|
||||
"showStroke": "Afficher le sélecteur de couleur de trait",
|
||||
"showBackground": "Afficher le sélecteur de couleur de fond",
|
||||
|
@ -128,7 +123,8 @@
|
|||
"unlockAll": "Tout déverrouiller"
|
||||
},
|
||||
"statusPublished": "Publié",
|
||||
"sidebarLock": "Maintenir la barre latérale ouverte"
|
||||
"sidebarLock": "Maintenir la barre latérale ouverte",
|
||||
"eyeDropper": ""
|
||||
},
|
||||
"library": {
|
||||
"noItems": "Aucun élément n'a encore été ajouté ...",
|
||||
|
@ -140,11 +136,7 @@
|
|||
"exportJSON": "Exporter comme fichier",
|
||||
"exportImage": "Exporter l'image...",
|
||||
"export": "Enregistrer sous...",
|
||||
"exportToPng": "Enregistrer en PNG",
|
||||
"exportToSvg": "Enregistrer en SVG",
|
||||
"copyToClipboard": "Copier dans le presse-papier",
|
||||
"copyPngToClipboard": "Copier le PNG dans le presse-papier",
|
||||
"scale": "Échelle",
|
||||
"save": "Enregistrer dans le fichier actuel",
|
||||
"saveAs": "Enregistrer sous",
|
||||
"load": "Ouvrir",
|
||||
|
@ -363,6 +355,30 @@
|
|||
"resetLibrary": "Réinitialiser la bibliothèque",
|
||||
"removeItemsFromLib": "Enlever les éléments sélectionnés de la bibliothèque"
|
||||
},
|
||||
"imageExportDialog": {
|
||||
"header": "",
|
||||
"label": {
|
||||
"withBackground": "",
|
||||
"onlySelected": "",
|
||||
"darkMode": "",
|
||||
"embedScene": "",
|
||||
"scale": "",
|
||||
"padding": ""
|
||||
},
|
||||
"tooltip": {
|
||||
"embedScene": ""
|
||||
},
|
||||
"title": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
},
|
||||
"button": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
}
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "Vos dessins sont chiffrés de bout en bout, les serveurs d'Excalidraw ne les verront jamais.",
|
||||
"link": "Article de blog sur le chiffrement de bout en bout dans Excalidraw"
|
||||
|
|
|
@ -40,10 +40,6 @@
|
|||
"arrowhead_triangle": "Triángulo",
|
||||
"fontSize": "Tamaño da fonte",
|
||||
"fontFamily": "Tipo de fonte",
|
||||
"onlySelected": "Só seleccionados",
|
||||
"withBackground": "Fondo",
|
||||
"exportEmbedScene": "Inserir escena",
|
||||
"exportEmbedScene_details": "Os datos da escena serán gardados no ficheiro PNG/SVG exportado polo que a escena poderá ser restaurada dende el. Isto aumentará o tamaño do ficheiro exportado.",
|
||||
"addWatermark": "Engadir \"Feito con Excalidraw\"",
|
||||
"handDrawn": "Debuxado a man",
|
||||
"normal": "Normal",
|
||||
|
@ -100,7 +96,6 @@
|
|||
"flipHorizontal": "Virar horizontalmente",
|
||||
"flipVertical": "Virar verticalmente",
|
||||
"viewMode": "Modo de visualización",
|
||||
"toggleExportColorScheme": "Alternar esquema de cores de exportación",
|
||||
"share": "Compartir",
|
||||
"showStroke": "Mostrar selector de cores do trazo",
|
||||
"showBackground": "Mostrar selector de cores do fondo",
|
||||
|
@ -128,7 +123,8 @@
|
|||
"unlockAll": "Desbloquear todo"
|
||||
},
|
||||
"statusPublished": "Publicado",
|
||||
"sidebarLock": "Manter a barra lateral aberta"
|
||||
"sidebarLock": "Manter a barra lateral aberta",
|
||||
"eyeDropper": ""
|
||||
},
|
||||
"library": {
|
||||
"noItems": "Aínda non hai elementos engadidos...",
|
||||
|
@ -140,11 +136,7 @@
|
|||
"exportJSON": "Exportar a arquivo",
|
||||
"exportImage": "Exportar imaxe...",
|
||||
"export": "Gardar en...",
|
||||
"exportToPng": "Exportar a PNG",
|
||||
"exportToSvg": "Exportar a SVG",
|
||||
"copyToClipboard": "Copiar ao portapapeis",
|
||||
"copyPngToClipboard": "Copiar PNG ao portapapeis",
|
||||
"scale": "Escala",
|
||||
"save": "Gardar no ficheiro actual",
|
||||
"saveAs": "Gardar como",
|
||||
"load": "Abrir",
|
||||
|
@ -363,6 +355,30 @@
|
|||
"resetLibrary": "Restablecer biblioteca",
|
||||
"removeItemsFromLib": "Eliminar os elementos seleccionados da biblioteca"
|
||||
},
|
||||
"imageExportDialog": {
|
||||
"header": "",
|
||||
"label": {
|
||||
"withBackground": "",
|
||||
"onlySelected": "",
|
||||
"darkMode": "",
|
||||
"embedScene": "",
|
||||
"scale": "",
|
||||
"padding": ""
|
||||
},
|
||||
"tooltip": {
|
||||
"embedScene": ""
|
||||
},
|
||||
"title": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
},
|
||||
"button": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
}
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "Os teus debuxos están cifrados de punto a punto, polo que os servidores de Excalidraw nunca os verán.",
|
||||
"link": "Entrada do blog acerca do cifrado de punto a punto en Excalidraw"
|
||||
|
|
|
@ -40,10 +40,6 @@
|
|||
"arrowhead_triangle": "משולש",
|
||||
"fontSize": "גודל גופן",
|
||||
"fontFamily": "גופן",
|
||||
"onlySelected": "רק מה שנבחר",
|
||||
"withBackground": "רקע",
|
||||
"exportEmbedScene": "הטמעה של מידע הסצנה",
|
||||
"exportEmbedScene_details": "הייצוא יבוצע לקובץ מסוג PNG/SVG כדי שהמידע על הסצנה ישמר בו וניתן יהיה לבצע שחזור ממנו.\nיגדיל את גודל הקובץ של הייצוא.",
|
||||
"addWatermark": "הוסף \"נוצר באמצעות Excalidraw\"",
|
||||
"handDrawn": "ציור יד",
|
||||
"normal": "רגיל",
|
||||
|
@ -100,7 +96,6 @@
|
|||
"flipHorizontal": "הפוך אופקית",
|
||||
"flipVertical": "הפוך אנכית",
|
||||
"viewMode": "מצב תצוגה",
|
||||
"toggleExportColorScheme": "מתג ערכת צבעים לייצוא",
|
||||
"share": "שתף",
|
||||
"showStroke": "הצג בוחר צבע מברשת",
|
||||
"showBackground": "הצג בוחר צבע רקע",
|
||||
|
@ -128,7 +123,8 @@
|
|||
"unlockAll": "שחרור הכול"
|
||||
},
|
||||
"statusPublished": "פורסם",
|
||||
"sidebarLock": "שמור את סרגל הצד פתוח"
|
||||
"sidebarLock": "שמור את סרגל הצד פתוח",
|
||||
"eyeDropper": ""
|
||||
},
|
||||
"library": {
|
||||
"noItems": "עוד לא הוספת דברים...",
|
||||
|
@ -140,11 +136,7 @@
|
|||
"exportJSON": "ייצא לקובץ",
|
||||
"exportImage": "ייצוא התמונה...",
|
||||
"export": "שמור ל...",
|
||||
"exportToPng": "יצא ל PNG",
|
||||
"exportToSvg": "יצא ל SVG",
|
||||
"copyToClipboard": "העתק ללוח",
|
||||
"copyPngToClipboard": "העתק PNG ללוח",
|
||||
"scale": "קנה מידה",
|
||||
"save": "שמור לקובץ נוכחי",
|
||||
"saveAs": "שמירה בשם",
|
||||
"load": "פתח",
|
||||
|
@ -363,6 +355,30 @@
|
|||
"resetLibrary": "איפוס ספריה",
|
||||
"removeItemsFromLib": "הסר את הפריטים הנבחרים מהספריה"
|
||||
},
|
||||
"imageExportDialog": {
|
||||
"header": "",
|
||||
"label": {
|
||||
"withBackground": "",
|
||||
"onlySelected": "",
|
||||
"darkMode": "",
|
||||
"embedScene": "",
|
||||
"scale": "",
|
||||
"padding": ""
|
||||
},
|
||||
"tooltip": {
|
||||
"embedScene": ""
|
||||
},
|
||||
"title": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
},
|
||||
"button": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
}
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "הציורים שלך מוצפנים מקצה לקצה כך שהשרתים של Excalidraw לא יראו אותם לעולם.",
|
||||
"link": "פוסט בבלוג על הצפנה מקצה לקצב ב-Excalidraw"
|
||||
|
|
|
@ -40,10 +40,6 @@
|
|||
"arrowhead_triangle": "त्रिकोण",
|
||||
"fontSize": "फ़ॉन्ट का आकार",
|
||||
"fontFamily": "फ़ॉन्ट का परिवार",
|
||||
"onlySelected": "केवल चयनित",
|
||||
"withBackground": "पृष्ठभूमि",
|
||||
"exportEmbedScene": "",
|
||||
"exportEmbedScene_details": "निर्यात एम्बेड दृश्य विवरण",
|
||||
"addWatermark": "ऐड \"मेड विथ एक्सकैलिडराव\"",
|
||||
"handDrawn": "हाथ से बनाया हुआ",
|
||||
"normal": "साधारण",
|
||||
|
@ -100,7 +96,6 @@
|
|||
"flipHorizontal": "दायें बायें पलटे",
|
||||
"flipVertical": "ऊपर नीचे पलटे",
|
||||
"viewMode": "अलग अलग देखें",
|
||||
"toggleExportColorScheme": "",
|
||||
"share": "शेयर करें",
|
||||
"showStroke": "",
|
||||
"showBackground": "पृष्ठभूमि रंग वरक़ दिखाये",
|
||||
|
@ -128,7 +123,8 @@
|
|||
"unlockAll": "सब ताले के बाहर निकाले"
|
||||
},
|
||||
"statusPublished": "प्रकाशित",
|
||||
"sidebarLock": "साइडबार खुला रखे."
|
||||
"sidebarLock": "साइडबार खुला रखे.",
|
||||
"eyeDropper": ""
|
||||
},
|
||||
"library": {
|
||||
"noItems": "अभी तक कोई आइटम जोडा नहीं गया.",
|
||||
|
@ -140,11 +136,7 @@
|
|||
"exportJSON": "",
|
||||
"exportImage": "प्रतिमा निर्यात करे...",
|
||||
"export": "यंहा सुरक्षित करे...",
|
||||
"exportToPng": "पीएनजी के रूप में निर्यात करे",
|
||||
"exportToSvg": "Svg के रूप में निर्यात करे",
|
||||
"copyToClipboard": "क्लिपबोर्ड पर प्रतिलिपि बनाएँ",
|
||||
"copyPngToClipboard": "क्लिपबोर्ड पर कॉपी करें,पीएनजी के रूप में",
|
||||
"scale": "पैमाना",
|
||||
"save": "",
|
||||
"saveAs": "सेव करे इस तरह",
|
||||
"load": "खोलें",
|
||||
|
@ -208,10 +200,10 @@
|
|||
"collabSaveFailed": "किसी कारण वश अंदरूनी डेटाबेस में सहेजा नहीं जा सका। यदि समस्या बनी रहती है, तो किये काम को खोने न देने के लिये अपनी फ़ाइल को स्थानीय रूप से सहेजे।",
|
||||
"collabSaveFailed_sizeExceeded": "लगता है कि पृष्ठ तल काफ़ी बड़ा है, इस्कारण अंदरूनी डेटाबेस में सहेजा नहीं जा सका। किये काम को खोने न देने के लिये अपनी फ़ाइल को स्थानीय रूप से सहेजे।",
|
||||
"brave_measure_text_error": {
|
||||
"line1": "",
|
||||
"line2": "",
|
||||
"line3": "",
|
||||
"line4": ""
|
||||
"line1": "लगता है कि आप Brave ब्राउज़र का उपयोग कर रहे और साथ में <bold>आक्रामक उँगलियो के छाप </bold> का चयन किया हुवा है",
|
||||
"line2": "यह आपके चित्रों के <bold>पाठ तत्वों</bold>को खंडित कर सकता हैं",
|
||||
"line3": "हमें आपसे ठोस आग्रह है की आप सेट्टिंग में इस विकल्प का चयन ना करे.<link> इस अनुक्रम </link> का पालन करके इसका पता लगा सकते हैं",
|
||||
"line4": "यदि इस सेटिंग्स को अक्षम करने पर भी पृष्ठ ठीक नहीं दिखता हो तो, हमारे GitHub पर एक <issueLink>मुद्दा प्रस्तुत</issueLink> करे, या हमें <discordLink>डिस्कोर्ड</discordLink> पर लिखित सम्पर्क करें"
|
||||
}
|
||||
},
|
||||
"toolBar": {
|
||||
|
@ -363,6 +355,30 @@
|
|||
"resetLibrary": "",
|
||||
"removeItemsFromLib": ""
|
||||
},
|
||||
"imageExportDialog": {
|
||||
"header": "",
|
||||
"label": {
|
||||
"withBackground": "",
|
||||
"onlySelected": "",
|
||||
"darkMode": "",
|
||||
"embedScene": "",
|
||||
"scale": "",
|
||||
"padding": ""
|
||||
},
|
||||
"tooltip": {
|
||||
"embedScene": ""
|
||||
},
|
||||
"title": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
},
|
||||
"button": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
}
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "आपके चित्र अंत-से-अंत एन्क्रिप्टेड हैं, इसलिए एक्सक्लूसिव्रॉव के सर्वर उन्हें कभी नहीं देखेंगे।",
|
||||
"link": ""
|
||||
|
@ -395,20 +411,20 @@
|
|||
},
|
||||
"colors": {
|
||||
"transparent": "",
|
||||
"black": "",
|
||||
"white": "",
|
||||
"red": "",
|
||||
"pink": "",
|
||||
"grape": "",
|
||||
"violet": "",
|
||||
"gray": "",
|
||||
"blue": "",
|
||||
"cyan": "",
|
||||
"teal": "",
|
||||
"green": "",
|
||||
"yellow": "",
|
||||
"orange": "",
|
||||
"bronze": ""
|
||||
"black": "काला",
|
||||
"white": "सफ़ेद",
|
||||
"red": "लाल",
|
||||
"pink": "गुलाबी",
|
||||
"grape": "अंगूरी",
|
||||
"violet": "जामुनी",
|
||||
"gray": "गहरा",
|
||||
"blue": "नीला",
|
||||
"cyan": "आसमानी",
|
||||
"teal": "हरा-नीला",
|
||||
"green": "हरा",
|
||||
"yellow": "पीला",
|
||||
"orange": "नारंगी",
|
||||
"bronze": "कांस्य"
|
||||
},
|
||||
"welcomeScreen": {
|
||||
"app": {
|
||||
|
@ -424,10 +440,10 @@
|
|||
}
|
||||
},
|
||||
"colorPicker": {
|
||||
"mostUsedCustomColors": "",
|
||||
"colors": "",
|
||||
"shades": "",
|
||||
"hexCode": "",
|
||||
"noShades": ""
|
||||
"mostUsedCustomColors": "अधिकांश उपयोगित रंग",
|
||||
"colors": "रंग",
|
||||
"shades": "छाया",
|
||||
"hexCode": "हेक्स कोड",
|
||||
"noShades": "इस रंग की कोई छाया उपलब्ध नहीं हैं"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,10 +40,6 @@
|
|||
"arrowhead_triangle": "Háromszög",
|
||||
"fontSize": "Betűméret",
|
||||
"fontFamily": "Betűkészlet család",
|
||||
"onlySelected": "Csak a kijelölt",
|
||||
"withBackground": "Háttér",
|
||||
"exportEmbedScene": "Jelenet beágyazása",
|
||||
"exportEmbedScene_details": "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.",
|
||||
"addWatermark": "Add hozzá, hogy \"Excalidraw-val készült\"",
|
||||
"handDrawn": "Kézzel rajzolt",
|
||||
"normal": "Normál",
|
||||
|
@ -100,7 +96,6 @@
|
|||
"flipHorizontal": "Vízszintes tükrözés",
|
||||
"flipVertical": "Függőleges tükrözés",
|
||||
"viewMode": "Nézet",
|
||||
"toggleExportColorScheme": "Exportált színséma váltása",
|
||||
"share": "Megosztás",
|
||||
"showStroke": "Körvonal színválasztó megjelenítése",
|
||||
"showBackground": "Háttérszín-választó megjelenítése",
|
||||
|
@ -128,7 +123,8 @@
|
|||
"unlockAll": ""
|
||||
},
|
||||
"statusPublished": "",
|
||||
"sidebarLock": ""
|
||||
"sidebarLock": "",
|
||||
"eyeDropper": ""
|
||||
},
|
||||
"library": {
|
||||
"noItems": "",
|
||||
|
@ -140,11 +136,7 @@
|
|||
"exportJSON": "Exportálás fájlba",
|
||||
"exportImage": "",
|
||||
"export": "",
|
||||
"exportToPng": "Exportálás PNG-be",
|
||||
"exportToSvg": "Exportálás SVG-be",
|
||||
"copyToClipboard": "Vágólapra másolás",
|
||||
"copyPngToClipboard": "PNG másolása a vágólapra",
|
||||
"scale": "Nagyítás",
|
||||
"save": "Mentés az aktuális fájlba",
|
||||
"saveAs": "Mentés másként",
|
||||
"load": "",
|
||||
|
@ -363,6 +355,30 @@
|
|||
"resetLibrary": "Könyvtár alaphelyzetbe állítása",
|
||||
"removeItemsFromLib": "A kiválasztott elemek eltávolítása a könyvtárból"
|
||||
},
|
||||
"imageExportDialog": {
|
||||
"header": "",
|
||||
"label": {
|
||||
"withBackground": "",
|
||||
"onlySelected": "",
|
||||
"darkMode": "",
|
||||
"embedScene": "",
|
||||
"scale": "",
|
||||
"padding": ""
|
||||
},
|
||||
"tooltip": {
|
||||
"embedScene": ""
|
||||
},
|
||||
"title": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
},
|
||||
"button": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
}
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "A rajzaidat végpontok közötti titkosítással tároljuk, tehát az Excalidraw szervereiről se tud más belenézni.",
|
||||
"link": "Blogbejegyzés a végpontok közötti titkosításról az Excalidraw-ban"
|
||||
|
|
|
@ -40,10 +40,6 @@
|
|||
"arrowhead_triangle": "Segitiga",
|
||||
"fontSize": "Ukuran font",
|
||||
"fontFamily": "Jenis font",
|
||||
"onlySelected": "Hanya yang Dipilih",
|
||||
"withBackground": "Latar",
|
||||
"exportEmbedScene": "Sematkan pemandangan",
|
||||
"exportEmbedScene_details": "Data pemandangan akan disimpan dalam file PNG/SVG yang diekspor, sehingga pemandangan itu dapat dipulihkan darinya.\nAkan membesarkan ukuran file yang diekspor.",
|
||||
"addWatermark": "Tambahkan \"Dibuat dengan Excalidraw\"",
|
||||
"handDrawn": "Tulisan tangan",
|
||||
"normal": "Normal",
|
||||
|
@ -54,7 +50,7 @@
|
|||
"veryLarge": "Sangat besar",
|
||||
"solid": "Padat",
|
||||
"hachure": "Garis-garis",
|
||||
"zigzag": "",
|
||||
"zigzag": "Zigzag",
|
||||
"crossHatch": "Asiran silang",
|
||||
"thin": "Lembut",
|
||||
"bold": "Tebal",
|
||||
|
@ -100,7 +96,6 @@
|
|||
"flipHorizontal": "Balikkan horizontal",
|
||||
"flipVertical": "Balikkan vertikal",
|
||||
"viewMode": "Mode tampilan",
|
||||
"toggleExportColorScheme": "Ubah skema warna ekspor",
|
||||
"share": "Bagikan",
|
||||
"showStroke": "Tampilkan garis pengambil warna",
|
||||
"showBackground": "Tampilkan latar pengambil warna",
|
||||
|
@ -111,7 +106,7 @@
|
|||
"increaseFontSize": "Besarkan ukuran font",
|
||||
"unbindText": "Lepas teks",
|
||||
"bindText": "Kunci teks ke kontainer",
|
||||
"createContainerFromText": "",
|
||||
"createContainerFromText": "Bungkus teks dalam kontainer",
|
||||
"link": {
|
||||
"edit": "Edit tautan",
|
||||
"create": "Buat tautan",
|
||||
|
@ -128,7 +123,8 @@
|
|||
"unlockAll": "Lepas semua"
|
||||
},
|
||||
"statusPublished": "Telah terbit",
|
||||
"sidebarLock": "Biarkan sidebar tetap terbuka"
|
||||
"sidebarLock": "Biarkan sidebar tetap terbuka",
|
||||
"eyeDropper": ""
|
||||
},
|
||||
"library": {
|
||||
"noItems": "Belum ada item yang ditambahkan...",
|
||||
|
@ -140,11 +136,7 @@
|
|||
"exportJSON": "Ekspor ke file",
|
||||
"exportImage": "Ekspor gambar...",
|
||||
"export": "Simpan ke...",
|
||||
"exportToPng": "Ekspor ke PNG",
|
||||
"exportToSvg": "Ekspor ke SVG",
|
||||
"copyToClipboard": "Salin ke Papan Klip",
|
||||
"copyPngToClipboard": "Salin PNG ke papan klip",
|
||||
"scale": "Skala",
|
||||
"save": "Simpan ke file sekarang",
|
||||
"saveAs": "Simpan sebagai",
|
||||
"load": "Buka",
|
||||
|
@ -208,10 +200,10 @@
|
|||
"collabSaveFailed": "Tidak dapat menyimpan ke dalam basis data server. Jika masih berlanjut, Anda sebaiknya simpan berkas Anda secara lokal untuk memastikan pekerjaan Anda tidak hilang.",
|
||||
"collabSaveFailed_sizeExceeded": "Tidak dapat menyimpan ke dalam basis data server, tampaknya ukuran kanvas terlalu besar. Anda sebaiknya simpan berkas Anda secara lokal untuk memastikan pekerjaan Anda tidak hilang.",
|
||||
"brave_measure_text_error": {
|
||||
"line1": "",
|
||||
"line2": "",
|
||||
"line3": "",
|
||||
"line4": ""
|
||||
"line1": "Sepertinya Anda menggunkan peramban Brave dengan pengaturan <bold>Blokir Fingerprinting yang Agresif</bold> diaktifkan.",
|
||||
"line2": "Ini dapat membuat <bold>Elemen Teks</bold> dalam gambar mu.",
|
||||
"line3": "Kami sangat menyarankan mematikan pengaturan ini. Anda dapat mengikuti <link>langkah-langkah ini</link> untuk melakukannya.",
|
||||
"line4": "Jika mematikan pengaturan ini tidak membenarkan tampilan elemen teks, mohon buka\n<issueLink>isu</issueLink> di GitHub kami, atau chat kami di <discordLink>Discord</discordLink>"
|
||||
}
|
||||
},
|
||||
"toolBar": {
|
||||
|
@ -229,7 +221,7 @@
|
|||
"penMode": "Mode pena - mencegah sentuhan",
|
||||
"link": "Tambah/Perbarui tautan untuk bentuk yang dipilih",
|
||||
"eraser": "Penghapus",
|
||||
"hand": ""
|
||||
"hand": "Tangan (alat panning)"
|
||||
},
|
||||
"headings": {
|
||||
"canvasActions": "Opsi Kanvas",
|
||||
|
@ -237,7 +229,7 @@
|
|||
"shapes": "Bentuk"
|
||||
},
|
||||
"hints": {
|
||||
"canvasPanning": "",
|
||||
"canvasPanning": "Untuk memindahkan kanvas, tekan roda mouse atau spacebar sambil menyeret, atau menggunakan alat tangan",
|
||||
"linearElement": "Klik untuk memulai banyak poin, seret untuk satu baris",
|
||||
"freeDraw": "Klik dan seret, lepaskan jika Anda selesai",
|
||||
"text": "Tip: Anda juga dapat menambahkan teks dengan klik ganda di mana saja dengan alat pemilihan",
|
||||
|
@ -306,8 +298,8 @@
|
|||
"doubleClick": "klik-ganda",
|
||||
"drag": "seret",
|
||||
"editor": "Editor",
|
||||
"editLineArrowPoints": "",
|
||||
"editText": "",
|
||||
"editLineArrowPoints": "Edit titik garis/panah",
|
||||
"editText": "Edit teks / tambah label",
|
||||
"github": "Menemukan masalah? Kirimkan",
|
||||
"howto": "Ikuti panduan kami",
|
||||
"or": "atau",
|
||||
|
@ -363,6 +355,30 @@
|
|||
"resetLibrary": "Reset pustaka",
|
||||
"removeItemsFromLib": "Hapus item yang dipilih dari pustaka"
|
||||
},
|
||||
"imageExportDialog": {
|
||||
"header": "",
|
||||
"label": {
|
||||
"withBackground": "",
|
||||
"onlySelected": "",
|
||||
"darkMode": "",
|
||||
"embedScene": "",
|
||||
"scale": "",
|
||||
"padding": ""
|
||||
},
|
||||
"tooltip": {
|
||||
"embedScene": ""
|
||||
},
|
||||
"title": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
},
|
||||
"button": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
}
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "Gambar anda terenkripsi end-to-end sehingga server Excalidraw tidak akan pernah dapat melihatnya.",
|
||||
"link": "Pos blog tentang enkripsi ujung ke ujung di Excalidraw"
|
||||
|
@ -395,20 +411,20 @@
|
|||
},
|
||||
"colors": {
|
||||
"transparent": "Transparan",
|
||||
"black": "",
|
||||
"white": "",
|
||||
"red": "",
|
||||
"pink": "",
|
||||
"grape": "",
|
||||
"violet": "",
|
||||
"gray": "",
|
||||
"blue": "",
|
||||
"cyan": "",
|
||||
"teal": "",
|
||||
"green": "",
|
||||
"yellow": "",
|
||||
"orange": "",
|
||||
"bronze": ""
|
||||
"black": "Hitam",
|
||||
"white": "Putih",
|
||||
"red": "Merah",
|
||||
"pink": "Pink",
|
||||
"grape": "Ungu",
|
||||
"violet": "Violet",
|
||||
"gray": "Abu-abu",
|
||||
"blue": "Biru",
|
||||
"cyan": "Cyan",
|
||||
"teal": "Teal",
|
||||
"green": "Hijau",
|
||||
"yellow": "Kuning",
|
||||
"orange": "Jingga",
|
||||
"bronze": "Tembaga"
|
||||
},
|
||||
"welcomeScreen": {
|
||||
"app": {
|
||||
|
@ -424,10 +440,10 @@
|
|||
}
|
||||
},
|
||||
"colorPicker": {
|
||||
"mostUsedCustomColors": "",
|
||||
"colors": "",
|
||||
"shades": "",
|
||||
"hexCode": "",
|
||||
"noShades": ""
|
||||
"mostUsedCustomColors": "Warna yang sering dipakai",
|
||||
"colors": "Warna",
|
||||
"shades": "Nuansa",
|
||||
"hexCode": "Kode hexa",
|
||||
"noShades": "Tidak ada nuansa untuk warna ini"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,10 +40,6 @@
|
|||
"arrowhead_triangle": "Triangolo",
|
||||
"fontSize": "Dimensione carattere",
|
||||
"fontFamily": "Carattere",
|
||||
"onlySelected": "Solo selezionati",
|
||||
"withBackground": "Sfondo",
|
||||
"exportEmbedScene": "Includi scena",
|
||||
"exportEmbedScene_details": "I dati della scena saranno salvati nel file PNG/SVG esportato in modo che la scena possa essere ripristinata da esso.\nQuesto aumenterà la dimensione del file esportato.",
|
||||
"addWatermark": "Aggiungi \"Creato con Excalidraw\"",
|
||||
"handDrawn": "A mano libera",
|
||||
"normal": "Normale",
|
||||
|
@ -100,7 +96,6 @@
|
|||
"flipHorizontal": "Capovolgi orizzontalmente",
|
||||
"flipVertical": "Capovolgi verticalmente",
|
||||
"viewMode": "Modalità visualizzazione",
|
||||
"toggleExportColorScheme": "Cambia lo schema di colori in esportazione",
|
||||
"share": "Condividi",
|
||||
"showStroke": "Mostra selettore colore del tratto",
|
||||
"showBackground": "Mostra selettore colore di sfondo",
|
||||
|
@ -111,7 +106,7 @@
|
|||
"increaseFontSize": "Aumenta la dimensione dei caratteri",
|
||||
"unbindText": "Scollega testo",
|
||||
"bindText": "Associa il testo al container",
|
||||
"createContainerFromText": "",
|
||||
"createContainerFromText": "Avvolgi il testo in un container",
|
||||
"link": {
|
||||
"edit": "Modifica link",
|
||||
"create": "Crea link",
|
||||
|
@ -128,7 +123,8 @@
|
|||
"unlockAll": "Sblocca tutto"
|
||||
},
|
||||
"statusPublished": "Pubblicato",
|
||||
"sidebarLock": "Mantieni aperta la barra laterale"
|
||||
"sidebarLock": "Mantieni aperta la barra laterale",
|
||||
"eyeDropper": ""
|
||||
},
|
||||
"library": {
|
||||
"noItems": "Nessun elemento ancora aggiunto...",
|
||||
|
@ -140,11 +136,7 @@
|
|||
"exportJSON": "Esporta su file",
|
||||
"exportImage": "Esporta immagine...",
|
||||
"export": "Salva in...",
|
||||
"exportToPng": "Esporta come PNG",
|
||||
"exportToSvg": "Esporta come SVG",
|
||||
"copyToClipboard": "Copia negli appunti",
|
||||
"copyPngToClipboard": "Copia PNG negli appunti",
|
||||
"scale": "Scala",
|
||||
"save": "Salva sul file corrente",
|
||||
"saveAs": "Salva con nome",
|
||||
"load": "Apri",
|
||||
|
@ -363,6 +355,30 @@
|
|||
"resetLibrary": "Ripristina la libreria",
|
||||
"removeItemsFromLib": "Rimuovi gli elementi selezionati dalla libreria"
|
||||
},
|
||||
"imageExportDialog": {
|
||||
"header": "Esporta immagine",
|
||||
"label": {
|
||||
"withBackground": "Sfondo",
|
||||
"onlySelected": "Solo selezionato",
|
||||
"darkMode": "Tema scuro",
|
||||
"embedScene": "",
|
||||
"scale": "Scala",
|
||||
"padding": ""
|
||||
},
|
||||
"tooltip": {
|
||||
"embedScene": ""
|
||||
},
|
||||
"title": {
|
||||
"exportToPng": "Esporta come PNG",
|
||||
"exportToSvg": "Esporta come SVG",
|
||||
"copyPngToClipboard": "Copia PNG negli appunti"
|
||||
},
|
||||
"button": {
|
||||
"exportToPng": "PNG",
|
||||
"exportToSvg": "SVG",
|
||||
"copyPngToClipboard": "Copia negli appunti"
|
||||
}
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "I tuoi disegni sono crittografati end-to-end in modo che i server di Excalidraw non li possano mai vedere.",
|
||||
"link": "Articolo del blog sulla crittografia end-to-end di Excalidraw"
|
||||
|
@ -395,20 +411,20 @@
|
|||
},
|
||||
"colors": {
|
||||
"transparent": "Trasparente",
|
||||
"black": "",
|
||||
"white": "",
|
||||
"red": "",
|
||||
"pink": "",
|
||||
"grape": "",
|
||||
"violet": "",
|
||||
"gray": "",
|
||||
"blue": "",
|
||||
"cyan": "",
|
||||
"teal": "",
|
||||
"green": "",
|
||||
"yellow": "",
|
||||
"orange": "",
|
||||
"bronze": ""
|
||||
"black": "Nero",
|
||||
"white": "Bianco",
|
||||
"red": "Rosso",
|
||||
"pink": "Rosa",
|
||||
"grape": "Uva",
|
||||
"violet": "Viola",
|
||||
"gray": "Grigio",
|
||||
"blue": "Blu",
|
||||
"cyan": "Ciano",
|
||||
"teal": "Verde acqua",
|
||||
"green": "Verde",
|
||||
"yellow": "Giallo",
|
||||
"orange": "Arancio",
|
||||
"bronze": "Bronzo"
|
||||
},
|
||||
"welcomeScreen": {
|
||||
"app": {
|
||||
|
@ -424,10 +440,10 @@
|
|||
}
|
||||
},
|
||||
"colorPicker": {
|
||||
"mostUsedCustomColors": "",
|
||||
"colors": "",
|
||||
"shades": "",
|
||||
"hexCode": "",
|
||||
"noShades": ""
|
||||
"mostUsedCustomColors": "Colori personalizzati più utilizzati",
|
||||
"colors": "Colori",
|
||||
"shades": "Sfumature",
|
||||
"hexCode": "Codice esadecimale",
|
||||
"noShades": "Nessuna sfumatura disponibile per questo colore"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,10 +40,6 @@
|
|||
"arrowhead_triangle": "三角",
|
||||
"fontSize": "フォントの大きさ",
|
||||
"fontFamily": "フォントの種類",
|
||||
"onlySelected": "選択中のみ",
|
||||
"withBackground": "背景",
|
||||
"exportEmbedScene": "埋め込みシーン",
|
||||
"exportEmbedScene_details": "シーンデータはエクスポートされたPNG/SVGファイルに保存され、シーンを復元することができます。\nエクスポートされたファイルのサイズは増加します。",
|
||||
"addWatermark": "\"Made with Excalidraw\"と表示",
|
||||
"handDrawn": "手描き風",
|
||||
"normal": "普通",
|
||||
|
@ -100,7 +96,6 @@
|
|||
"flipHorizontal": "水平方向に反転",
|
||||
"flipVertical": "垂直方向に反転",
|
||||
"viewMode": "閲覧モード",
|
||||
"toggleExportColorScheme": "エクスポートカラースキームの切り替え",
|
||||
"share": "共有",
|
||||
"showStroke": "ストロークカラーピッカーを表示",
|
||||
"showBackground": "背景色ピッカーを表示",
|
||||
|
@ -128,7 +123,8 @@
|
|||
"unlockAll": "すべてのロックを解除"
|
||||
},
|
||||
"statusPublished": "公開済み",
|
||||
"sidebarLock": "サイドバーを開いたままにする"
|
||||
"sidebarLock": "サイドバーを開いたままにする",
|
||||
"eyeDropper": ""
|
||||
},
|
||||
"library": {
|
||||
"noItems": "まだアイテムが追加されていません…",
|
||||
|
@ -140,11 +136,7 @@
|
|||
"exportJSON": "ファイルへエクスポート",
|
||||
"exportImage": "画像のエクスポート...",
|
||||
"export": "名前を付けて保存...",
|
||||
"exportToPng": "PNG にエクスポート",
|
||||
"exportToSvg": "SVG にエクスポート",
|
||||
"copyToClipboard": "クリップボードにコピー",
|
||||
"copyPngToClipboard": "クリップボードにPNGをコピー",
|
||||
"scale": "スケール",
|
||||
"save": "現在のファイルに保存",
|
||||
"saveAs": "名前を付けて保存",
|
||||
"load": "開く",
|
||||
|
@ -363,6 +355,30 @@
|
|||
"resetLibrary": "ライブラリをリセット",
|
||||
"removeItemsFromLib": "選択したアイテムをライブラリから削除"
|
||||
},
|
||||
"imageExportDialog": {
|
||||
"header": "",
|
||||
"label": {
|
||||
"withBackground": "",
|
||||
"onlySelected": "",
|
||||
"darkMode": "",
|
||||
"embedScene": "",
|
||||
"scale": "",
|
||||
"padding": ""
|
||||
},
|
||||
"tooltip": {
|
||||
"embedScene": ""
|
||||
},
|
||||
"title": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
},
|
||||
"button": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
}
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "描画内容はエンドツーエンド暗号化が施されており、Excalidrawサーバーが内容を見ることはできません。",
|
||||
"link": "Excalidrawのエンドツーエンド暗号化に関するブログ記事"
|
||||
|
|
|
@ -40,10 +40,6 @@
|
|||
"arrowhead_triangle": "",
|
||||
"fontSize": "Shrift ólshemi",
|
||||
"fontFamily": "",
|
||||
"onlySelected": "",
|
||||
"withBackground": "",
|
||||
"exportEmbedScene": "",
|
||||
"exportEmbedScene_details": "",
|
||||
"addWatermark": "",
|
||||
"handDrawn": "",
|
||||
"normal": "",
|
||||
|
@ -100,7 +96,6 @@
|
|||
"flipHorizontal": "",
|
||||
"flipVertical": "",
|
||||
"viewMode": "Kóriw rejimi",
|
||||
"toggleExportColorScheme": "",
|
||||
"share": "Bólisiw",
|
||||
"showStroke": "",
|
||||
"showBackground": "",
|
||||
|
@ -128,7 +123,8 @@
|
|||
"unlockAll": "Barlıǵın qulıptan shıǵarıw"
|
||||
},
|
||||
"statusPublished": "",
|
||||
"sidebarLock": ""
|
||||
"sidebarLock": "",
|
||||
"eyeDropper": ""
|
||||
},
|
||||
"library": {
|
||||
"noItems": "",
|
||||
|
@ -140,11 +136,7 @@
|
|||
"exportJSON": "",
|
||||
"exportImage": "",
|
||||
"export": "",
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyToClipboard": "Almasıw buferine kóshirip alındı",
|
||||
"copyPngToClipboard": "",
|
||||
"scale": "",
|
||||
"save": "",
|
||||
"saveAs": "",
|
||||
"load": "Ashıw",
|
||||
|
@ -363,6 +355,30 @@
|
|||
"resetLibrary": "",
|
||||
"removeItemsFromLib": ""
|
||||
},
|
||||
"imageExportDialog": {
|
||||
"header": "",
|
||||
"label": {
|
||||
"withBackground": "",
|
||||
"onlySelected": "",
|
||||
"darkMode": "",
|
||||
"embedScene": "",
|
||||
"scale": "",
|
||||
"padding": ""
|
||||
},
|
||||
"tooltip": {
|
||||
"embedScene": ""
|
||||
},
|
||||
"title": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
},
|
||||
"button": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
}
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "",
|
||||
"link": ""
|
||||
|
|
|
@ -40,10 +40,6 @@
|
|||
"arrowhead_triangle": "Akerdis",
|
||||
"fontSize": "Tiddi n tsefsit",
|
||||
"fontFamily": "Tawacult n tsefsiyin",
|
||||
"onlySelected": "Tafrayt kan",
|
||||
"withBackground": "Agilal",
|
||||
"exportEmbedScene": "Sleɣ asayes",
|
||||
"exportEmbedScene_details": "Asayes ad yettwasekles deg ufaylu n usifeḍ PNG/SVG akken akken ad yili wamek ara d-yettwarr seg-s usayes. Ayagi ad isimɣur tiddi n ufaylu n usifeḍ.",
|
||||
"addWatermark": "Seddu \"Yettwaxdem s Excalidraw\"",
|
||||
"handDrawn": "Asuneɣ s ufus",
|
||||
"normal": "Amagnu",
|
||||
|
@ -100,7 +96,6 @@
|
|||
"flipHorizontal": "Tuttya taglawant",
|
||||
"flipVertical": "Tuttya tubdidt",
|
||||
"viewMode": "Askar n tmuɣli",
|
||||
"toggleExportColorScheme": "Sermed/sens asifeḍ usentel n yini",
|
||||
"share": "Bḍu",
|
||||
"showStroke": "Beqqeḍ amelqaḍ n yini n yizirig",
|
||||
"showBackground": "Beqqeḍ amelqaḍ n yini n ugilal",
|
||||
|
@ -128,7 +123,8 @@
|
|||
"unlockAll": "Serreḥ akk"
|
||||
},
|
||||
"statusPublished": "Yeffeɣ-d",
|
||||
"sidebarLock": "Eǧǧ afeggag n yidis yeldi"
|
||||
"sidebarLock": "Eǧǧ afeggag n yidis yeldi",
|
||||
"eyeDropper": ""
|
||||
},
|
||||
"library": {
|
||||
"noItems": "Ulac iferdisen yettwarnan yakan...",
|
||||
|
@ -140,11 +136,7 @@
|
|||
"exportJSON": "Sifeḍ afaylu",
|
||||
"exportImage": "Sifeḍ tugna...",
|
||||
"export": "Sekles di...",
|
||||
"exportToPng": "Sifeḍ ɣer PNG",
|
||||
"exportToSvg": "Sifeḍ ɣer SVG",
|
||||
"copyToClipboard": "Nɣel ɣer tecfawit",
|
||||
"copyPngToClipboard": "Nɣel PNG ɣer tecfawit",
|
||||
"scale": "Taskala",
|
||||
"save": "Sekles deg ufaylu amiran",
|
||||
"saveAs": "Sekles am",
|
||||
"load": "Ldi",
|
||||
|
@ -363,6 +355,30 @@
|
|||
"resetLibrary": "Ales awennez n temkarḍit",
|
||||
"removeItemsFromLib": "Kkes iferdisen yettafernen si temkarḍit"
|
||||
},
|
||||
"imageExportDialog": {
|
||||
"header": "",
|
||||
"label": {
|
||||
"withBackground": "",
|
||||
"onlySelected": "",
|
||||
"darkMode": "",
|
||||
"embedScene": "",
|
||||
"scale": "",
|
||||
"padding": ""
|
||||
},
|
||||
"tooltip": {
|
||||
"embedScene": ""
|
||||
},
|
||||
"title": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
},
|
||||
"button": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
}
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "Unuɣen-inek (m) ttuwgelhnen seg yixef s ixef dɣa iqeddacen n Excalidraw werǧin ad ten-walin. ",
|
||||
"link": "Amagrad ɣef uwgelhen ixef s ixef di Excalidraw"
|
||||
|
|
|
@ -40,10 +40,6 @@
|
|||
"arrowhead_triangle": "",
|
||||
"fontSize": "Қаріп өлшемі",
|
||||
"fontFamily": "Қаріп тобы",
|
||||
"onlySelected": "",
|
||||
"withBackground": "",
|
||||
"exportEmbedScene": "",
|
||||
"exportEmbedScene_details": "",
|
||||
"addWatermark": "",
|
||||
"handDrawn": "",
|
||||
"normal": "Қалыпты",
|
||||
|
@ -100,7 +96,6 @@
|
|||
"flipHorizontal": "",
|
||||
"flipVertical": "",
|
||||
"viewMode": "",
|
||||
"toggleExportColorScheme": "",
|
||||
"share": "",
|
||||
"showStroke": "",
|
||||
"showBackground": "",
|
||||
|
@ -128,7 +123,8 @@
|
|||
"unlockAll": ""
|
||||
},
|
||||
"statusPublished": "",
|
||||
"sidebarLock": ""
|
||||
"sidebarLock": "",
|
||||
"eyeDropper": ""
|
||||
},
|
||||
"library": {
|
||||
"noItems": "",
|
||||
|
@ -140,11 +136,7 @@
|
|||
"exportJSON": "",
|
||||
"exportImage": "",
|
||||
"export": "",
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyToClipboard": "",
|
||||
"copyPngToClipboard": "",
|
||||
"scale": "",
|
||||
"save": "",
|
||||
"saveAs": "",
|
||||
"load": "",
|
||||
|
@ -363,6 +355,30 @@
|
|||
"resetLibrary": "",
|
||||
"removeItemsFromLib": ""
|
||||
},
|
||||
"imageExportDialog": {
|
||||
"header": "",
|
||||
"label": {
|
||||
"withBackground": "",
|
||||
"onlySelected": "",
|
||||
"darkMode": "",
|
||||
"embedScene": "",
|
||||
"scale": "",
|
||||
"padding": ""
|
||||
},
|
||||
"tooltip": {
|
||||
"embedScene": ""
|
||||
},
|
||||
"title": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
},
|
||||
"button": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
}
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "Сіздің сызбаларыңыз өтпелі шифрлеу арқылы шифрланған, сондықтан Excalidraw серверлері оларды ешқашан көрмейді.",
|
||||
"link": "Excalidraw қолданатын өтпелі шифрлеу туралы блог жазбасы"
|
||||
|
|
|
@ -1,414 +1,430 @@
|
|||
{
|
||||
"labels": {
|
||||
"paste": "",
|
||||
"pasteAsPlaintext": "",
|
||||
"pasteCharts": "",
|
||||
"selectAll": "",
|
||||
"multiSelect": "",
|
||||
"moveCanvas": "",
|
||||
"cut": "",
|
||||
"copy": "",
|
||||
"copyAsPng": "",
|
||||
"copyAsSvg": "",
|
||||
"copyText": "",
|
||||
"bringForward": "",
|
||||
"sendToBack": "",
|
||||
"bringToFront": "",
|
||||
"sendBackward": "",
|
||||
"delete": "",
|
||||
"copyStyles": "",
|
||||
"pasteStyles": "",
|
||||
"stroke": "",
|
||||
"background": "",
|
||||
"fill": "",
|
||||
"strokeWidth": "",
|
||||
"strokeStyle": "",
|
||||
"strokeStyle_solid": "",
|
||||
"strokeStyle_dashed": "",
|
||||
"strokeStyle_dotted": "",
|
||||
"sloppiness": "",
|
||||
"opacity": "",
|
||||
"textAlign": "",
|
||||
"edges": "",
|
||||
"sharp": "",
|
||||
"round": "",
|
||||
"arrowheads": "",
|
||||
"arrowhead_none": "",
|
||||
"arrowhead_arrow": "",
|
||||
"arrowhead_bar": "",
|
||||
"arrowhead_dot": "",
|
||||
"arrowhead_triangle": "",
|
||||
"fontSize": "",
|
||||
"fontFamily": "",
|
||||
"onlySelected": "",
|
||||
"withBackground": "",
|
||||
"exportEmbedScene": "",
|
||||
"exportEmbedScene_details": "",
|
||||
"addWatermark": "",
|
||||
"handDrawn": "",
|
||||
"normal": "",
|
||||
"code": "",
|
||||
"small": "",
|
||||
"medium": "",
|
||||
"large": "",
|
||||
"veryLarge": "",
|
||||
"solid": "",
|
||||
"hachure": "",
|
||||
"zigzag": "",
|
||||
"crossHatch": "",
|
||||
"thin": "",
|
||||
"bold": "",
|
||||
"left": "",
|
||||
"center": "",
|
||||
"right": "",
|
||||
"extraBold": "",
|
||||
"architect": "",
|
||||
"artist": "",
|
||||
"cartoonist": "",
|
||||
"fileTitle": "",
|
||||
"colorPicker": "",
|
||||
"canvasColors": "",
|
||||
"canvasBackground": "",
|
||||
"drawingCanvas": "",
|
||||
"layers": "",
|
||||
"actions": "",
|
||||
"language": "",
|
||||
"liveCollaboration": "",
|
||||
"duplicateSelection": "",
|
||||
"untitled": "",
|
||||
"name": "",
|
||||
"yourName": "",
|
||||
"madeWithExcalidraw": "",
|
||||
"group": "",
|
||||
"ungroup": "",
|
||||
"collaborators": "",
|
||||
"showGrid": "",
|
||||
"addToLibrary": "",
|
||||
"removeFromLibrary": "",
|
||||
"libraryLoadingMessage": "",
|
||||
"libraries": "",
|
||||
"loadingScene": "",
|
||||
"align": "",
|
||||
"alignTop": "",
|
||||
"alignBottom": "",
|
||||
"alignLeft": "",
|
||||
"alignRight": "",
|
||||
"centerVertically": "",
|
||||
"centerHorizontally": "",
|
||||
"distributeHorizontally": "",
|
||||
"distributeVertically": "",
|
||||
"flipHorizontal": "",
|
||||
"flipVertical": "",
|
||||
"viewMode": "",
|
||||
"toggleExportColorScheme": "",
|
||||
"share": "",
|
||||
"showStroke": "",
|
||||
"showBackground": "",
|
||||
"toggleTheme": "",
|
||||
"personalLib": "",
|
||||
"excalidrawLib": "",
|
||||
"decreaseFontSize": "",
|
||||
"increaseFontSize": "",
|
||||
"unbindText": "",
|
||||
"bindText": "",
|
||||
"createContainerFromText": "",
|
||||
"paste": "បិទភ្ជាប់",
|
||||
"pasteAsPlaintext": "បិទភ្ជាប់ជាអត្ថបទធម្មតា",
|
||||
"pasteCharts": "បិទភ្ជាប់តារាង",
|
||||
"selectAll": "ជ្រើសរើសទាំងអស់",
|
||||
"multiSelect": "បន្ថែមធាតុទៅលើការជ្រើសរើស",
|
||||
"moveCanvas": "ផ្លាស់ទីបាវ",
|
||||
"cut": "កាត់",
|
||||
"copy": "ចម្លង",
|
||||
"copyAsPng": "ចម្លងទៅក្តារតម្បៀតខ្ទាស់ជា PNG",
|
||||
"copyAsSvg": "ចម្លងទៅក្តារតម្បៀតខ្ទាស់ជា SVG",
|
||||
"copyText": "ចម្លងទៅក្តារតម្បៀតខ្ទាស់ជាអត្ថបទ",
|
||||
"bringForward": "នាំយកទៅលើ",
|
||||
"sendToBack": "នាំយកទៅក្រោយបង្អស់",
|
||||
"bringToFront": "នាំយកទៅលើបង្អស់",
|
||||
"sendBackward": "នាំយកទៅក្រោយ",
|
||||
"delete": "លុប",
|
||||
"copyStyles": "ចម្លងរចនាប័ទ្ម",
|
||||
"pasteStyles": "បិទភ្ជាប់រចនាប័ទ្ម",
|
||||
"stroke": "ខ្វាច់",
|
||||
"background": "ផ្ទៃខាងក្រោយ",
|
||||
"fill": "បំពេញ",
|
||||
"strokeWidth": "ទទឹងខ្វាច់",
|
||||
"strokeStyle": "រចនាប័ទ្មរបស់ខ្វាច់",
|
||||
"strokeStyle_solid": "តាន់",
|
||||
"strokeStyle_dashed": "ដាច់ៗ",
|
||||
"strokeStyle_dotted": "ចំណុចៗ",
|
||||
"sloppiness": "រចនាប័ទ្មបន្ទាត់",
|
||||
"opacity": "ភាពច្បាស់",
|
||||
"textAlign": "តម្រឹមអត្ថបទ",
|
||||
"edges": "គែម",
|
||||
"sharp": "មុត",
|
||||
"round": "រាងមូល",
|
||||
"arrowheads": "ក្បាលព្រួញ",
|
||||
"arrowhead_none": "គ្មាន",
|
||||
"arrowhead_arrow": "ព្រួញ",
|
||||
"arrowhead_bar": "របារ",
|
||||
"arrowhead_dot": "ចំណុច",
|
||||
"arrowhead_triangle": "ត្រីកោណ",
|
||||
"fontSize": "ទំហំពុម្ពអក្សរ",
|
||||
"fontFamily": "ក្រុមពុម្ពអក្សរ",
|
||||
"addWatermark": "បន្ថែមវ៉ាត់ធើម៉ាក \"Made with Excalidraw\"",
|
||||
"handDrawn": "គូរដោយដៃ",
|
||||
"normal": "ធម្មតា",
|
||||
"code": "កូដ",
|
||||
"small": "តូច",
|
||||
"medium": "មធ្យម",
|
||||
"large": "ធំ",
|
||||
"veryLarge": "ធំខ្លាំង",
|
||||
"solid": "តាន់",
|
||||
"hachure": "Hachure",
|
||||
"zigzag": "Zigzag",
|
||||
"crossHatch": "បន្ទាត់ឆ្នូតៗ",
|
||||
"thin": "ស្តើង",
|
||||
"bold": "ដឹត",
|
||||
"left": "ខាងឆ្វេង",
|
||||
"center": "កណ្ដាល",
|
||||
"right": "នៅខាងស្ដាំ",
|
||||
"extraBold": "ដិតបន្ថែម",
|
||||
"architect": "ស្ថាបត្យករ",
|
||||
"artist": "សិល្បៈករ",
|
||||
"cartoonist": "អ្នកគំនូរជីវចល",
|
||||
"fileTitle": "ឈ្មោះឯកសារ",
|
||||
"colorPicker": "ឧបករណ៍ជ្រើសពណ៌",
|
||||
"canvasColors": "ប្រើលើបាវ",
|
||||
"canvasBackground": "ផ្ទៃខាងក្រោយបាវ",
|
||||
"drawingCanvas": "តំបន់គំនូរ",
|
||||
"layers": "ស្រទាប់",
|
||||
"actions": "សកម្មភាព",
|
||||
"language": "ភាសា",
|
||||
"liveCollaboration": "សហការគ្នាផ្ទាល់...",
|
||||
"duplicateSelection": "ចម្លង",
|
||||
"untitled": "គ្មានឈ្មោះ",
|
||||
"name": "ឈ្មោះ",
|
||||
"yourName": "ឈ្មោះរបស់អ្នក",
|
||||
"madeWithExcalidraw": "បង្កើតជាមួយ Excalidraw",
|
||||
"group": "ការជ្រើសរើសជាក្រុម",
|
||||
"ungroup": "បំបែកក្រុមការជ្រើសរើសជាក្រុម",
|
||||
"collaborators": "អ្នកសហការ",
|
||||
"showGrid": "បង្ហាញក្រឡាចត្រង្គ",
|
||||
"addToLibrary": "បន្ថែមទៅបណ្ណាល័យ",
|
||||
"removeFromLibrary": "លុបចេញពីបណ្ណាល័យ",
|
||||
"libraryLoadingMessage": "កំពុងផ្ទុកបណ្ណាល័យ...",
|
||||
"libraries": "រកមើលបណ្ណាល័យ",
|
||||
"loadingScene": "កំពុងផ្ទុកស៊ីន...",
|
||||
"align": "តម្រឹម",
|
||||
"alignTop": "តម្រឹមផ្នែកខាងលើ",
|
||||
"alignBottom": "តម្រឹមផ្នែកខាងក្រោម",
|
||||
"alignLeft": "តម្រឹមឆ្វេង",
|
||||
"alignRight": "តម្រឹមស្តាំ",
|
||||
"centerVertically": "កណ្តាលបញ្ឈរ",
|
||||
"centerHorizontally": "កណ្តាលផ្ដេក",
|
||||
"distributeHorizontally": "ចែកចាយផ្ដេក",
|
||||
"distributeVertically": "ចែកចាយបញ្ឈរ",
|
||||
"flipHorizontal": "ត្រឡប់ដោយផ្ដេក",
|
||||
"flipVertical": "ត្រឡប់ដោយបញ្ឈរ",
|
||||
"viewMode": "ម៉ូដបង្ហាញ",
|
||||
"share": "ចែករំលែក",
|
||||
"showStroke": "បង្ហាញឧបករណ៍ជ្រើសរើសពណ៌ខ្វាច់",
|
||||
"showBackground": "បង្ហាញឧបករណ៍ជ្រើសរើសពណ៌ផ្ទៃខាងក្រោយ",
|
||||
"toggleTheme": "បិទ/បើកប្រធានបទ",
|
||||
"personalLib": "បណ្ណាល័យផ្ទាល់ខ្លួន",
|
||||
"excalidrawLib": "បណ្ណាល័យ Excalidraw",
|
||||
"decreaseFontSize": "បន្ថយទំហំពុម្ពអក្សរ",
|
||||
"increaseFontSize": "បង្កើនទំហំពុម្ពអក្សរ",
|
||||
"unbindText": "ស្រាយអត្ថបទ",
|
||||
"bindText": "ភ្ជាប់អត្ថបទទៅប្រអប់",
|
||||
"createContainerFromText": "រុំអត្ថបទក្នុងប្រអប់មួយ",
|
||||
"link": {
|
||||
"edit": "",
|
||||
"create": "",
|
||||
"label": ""
|
||||
"edit": "កែតំណភ្ជាប់",
|
||||
"create": "បង្កើតតំណភ្ជាប់",
|
||||
"label": "តំណ"
|
||||
},
|
||||
"lineEditor": {
|
||||
"edit": "",
|
||||
"exit": ""
|
||||
"edit": "កែសម្រួលបន្ទាត់",
|
||||
"exit": "ចាកចេញពីការកែសម្រួលបន្ទាត់"
|
||||
},
|
||||
"elementLock": {
|
||||
"lock": "",
|
||||
"unlock": "",
|
||||
"lockAll": "",
|
||||
"unlockAll": ""
|
||||
"lock": "ចាក់សោ",
|
||||
"unlock": "ដោះសោ",
|
||||
"lockAll": "ចាក់សោទាំងអស់",
|
||||
"unlockAll": "ដោះសោទាំងអស់"
|
||||
},
|
||||
"statusPublished": "",
|
||||
"sidebarLock": ""
|
||||
"statusPublished": "ត្រូវបានបោះពុម្ពផ្សាយ",
|
||||
"sidebarLock": "ទុករបារចំហៀងបើក",
|
||||
"eyeDropper": ""
|
||||
},
|
||||
"library": {
|
||||
"noItems": "",
|
||||
"hint_emptyLibrary": "",
|
||||
"hint_emptyPrivateLibrary": ""
|
||||
"noItems": "មិនទាន់មានធាតុបន្ថែមទេ...",
|
||||
"hint_emptyLibrary": "ជ្រើសរើសធាតុនៅលើបាវដើម្បីបន្ថែមវានៅទីនេះ ឬដំឡើងបណ្ណាល័យពីឃ្លាំងសាធារណៈខាងក្រោម។",
|
||||
"hint_emptyPrivateLibrary": "ជ្រើសរើសធាតុនៅលើបាវដើម្បីបន្ថែមវានៅទីនេះ"
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "",
|
||||
"exportJSON": "",
|
||||
"exportImage": "",
|
||||
"export": "",
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyToClipboard": "",
|
||||
"copyPngToClipboard": "",
|
||||
"scale": "",
|
||||
"save": "",
|
||||
"saveAs": "",
|
||||
"load": "",
|
||||
"getShareableLink": "",
|
||||
"close": "",
|
||||
"selectLanguage": "",
|
||||
"scrollBackToContent": "",
|
||||
"zoomIn": "",
|
||||
"zoomOut": "",
|
||||
"resetZoom": "",
|
||||
"menu": "",
|
||||
"done": "",
|
||||
"edit": "",
|
||||
"undo": "",
|
||||
"redo": "",
|
||||
"resetLibrary": "",
|
||||
"createNewRoom": "",
|
||||
"fullScreen": "",
|
||||
"darkMode": "",
|
||||
"lightMode": "",
|
||||
"zenMode": "",
|
||||
"exitZenMode": "",
|
||||
"cancel": "",
|
||||
"clear": "",
|
||||
"remove": "",
|
||||
"publishLibrary": "",
|
||||
"submit": "",
|
||||
"confirm": ""
|
||||
"clearReset": "កំណត់បាវឡើងវិញ",
|
||||
"exportJSON": "នាំចេញជាឯកសារ",
|
||||
"exportImage": "នាំរូបភាពចេញ",
|
||||
"export": "រក្សាទុកនៅ...",
|
||||
"copyToClipboard": "ចម្លងទៅក្តារតម្បៀតខ្ទាស់",
|
||||
"save": "រក្សាទុកទៅឯកសារបច្ចុប្បន្ន",
|
||||
"saveAs": "រក្សាទុកជា",
|
||||
"load": "បើក",
|
||||
"getShareableLink": "យកតំណដែលអាចចែករំលែកបាន",
|
||||
"close": "បិទ",
|
||||
"selectLanguage": "រើសភាសា",
|
||||
"scrollBackToContent": "រំកិលត្រឡប់ទៅមាតិកាវិញ",
|
||||
"zoomIn": "ពង្រីក",
|
||||
"zoomOut": "បង្រួម",
|
||||
"resetZoom": "កំណត់ការពង្រីកឡើងវិញ",
|
||||
"menu": "ម៉ឺនុយ",
|
||||
"done": "រួចរាល់",
|
||||
"edit": "កែ",
|
||||
"undo": "ត្រឡប់វិញ",
|
||||
"redo": "ធ្វើវិញ",
|
||||
"resetLibrary": "កំណត់បណ្ណាល័យឡើងវិញ",
|
||||
"createNewRoom": "បង្កើតបន្ទប់ថ្មី",
|
||||
"fullScreen": "ពេញអេក្រង់",
|
||||
"darkMode": "ម៉ូដងងឹត",
|
||||
"lightMode": "ម៉ូដភ្លឺ",
|
||||
"zenMode": "ម៉ូត Zen",
|
||||
"exitZenMode": "ចេញពី zen ម៉ូត",
|
||||
"cancel": "បោះបង់",
|
||||
"clear": "សម្អាត",
|
||||
"remove": "ដកចេញ",
|
||||
"publishLibrary": "បោះពុម្ពផ្សាយ",
|
||||
"submit": "ដាក់ស្នើ",
|
||||
"confirm": "បញ្ជាក់"
|
||||
},
|
||||
"alerts": {
|
||||
"clearReset": "",
|
||||
"couldNotCreateShareableLink": "",
|
||||
"couldNotCreateShareableLinkTooBig": "",
|
||||
"couldNotLoadInvalidFile": "",
|
||||
"importBackendFailed": "",
|
||||
"cannotExportEmptyCanvas": "",
|
||||
"couldNotCopyToClipboard": "",
|
||||
"decryptFailed": "",
|
||||
"uploadedSecurly": "",
|
||||
"loadSceneOverridePrompt": "",
|
||||
"collabStopOverridePrompt": "",
|
||||
"errorAddingToLibrary": "",
|
||||
"errorRemovingFromLibrary": "",
|
||||
"confirmAddLibrary": "",
|
||||
"imageDoesNotContainScene": "",
|
||||
"cannotRestoreFromImage": "",
|
||||
"invalidSceneUrl": "",
|
||||
"resetLibrary": "",
|
||||
"removeItemsFromsLibrary": "",
|
||||
"invalidEncryptionKey": "",
|
||||
"collabOfflineWarning": ""
|
||||
"clearReset": "វានឹងសម្អាតបាវទាំងមូល។ តើអ្នកប្រាកដឬអត់?",
|
||||
"couldNotCreateShareableLink": "មិនអាចបង្កើតតំណដែលអាចចែករំលែកបានទេ។",
|
||||
"couldNotCreateShareableLinkTooBig": "មិនអាចបង្កើតតំណដែលអាចចែករំលែកបាន៖ ស៊ីនធំពេក",
|
||||
"couldNotLoadInvalidFile": "មិនអាចផ្ទុកឯកសារមិនត្រឹមត្រូវបានទេ។",
|
||||
"importBackendFailed": "ការនាំចូលពីម៉ាស៊ីនមេបានបរាជ័យ។",
|
||||
"cannotExportEmptyCanvas": "មិនអាចនាំចេញបាវទទេបានទេ។",
|
||||
"couldNotCopyToClipboard": "មិនអាចចម្លងទៅក្ដារតម្បៀតខ្ទាស់បានទេ។",
|
||||
"decryptFailed": "មិនអាចឌិគ្រីបទិន្នន័យបានទេ។",
|
||||
"uploadedSecurly": "ការបង្ហោះត្រូវបានការពារដោយការអ៊ិនគ្រីបពីចុងដល់ចប់ មានន័យថា ទាំងម៉ាស៊ីនមេរបស់ Excalidraw ឬភាគីទីបីមិនអាចអានខ្លឹមសារបានទេ។",
|
||||
"loadSceneOverridePrompt": "ការផ្ទុកគំនូរខាងក្រៅនឹងជំនួសមាតិកាដែលមានស្រាប់របស់អ្នក។ តើអ្នកចង់បន្តទេ?",
|
||||
"collabStopOverridePrompt": "ការបញ្ឈប់សម័យនឹងសរសេរជាន់លើគំនូរដែលបានរក្សាទុកនៅលើកុំព្យូទ័ររបស់អ្នកពីមុន។ តើអ្នកប្រាកដឬអត់?\n\n(ប្រសិនបើអ្នកចង់រក្សាគំនូរដែលនៅលើកុំព្យូទ័ររបស់អ្នក គ្រាន់តែបិទផ្ទាំងកម្មវិធីរុករក។)",
|
||||
"errorAddingToLibrary": "មិនអាចបន្ថែមធាតុទៅបណ្ណាល័យបានទេ",
|
||||
"errorRemovingFromLibrary": "មិនអាចលុបធាតុចេញពីបណ្ណាល័យបានទេ",
|
||||
"confirmAddLibrary": "វានឹងបន្ថែមរូបរាង {{numShapes}} ទៅបណ្ណាល័យរបស់អ្នក។ តើអ្នកប្រាកដឬអត់?",
|
||||
"imageDoesNotContainScene": "រូបភាពនេះហាក់ដូចជាមិនមានទិន្នន័យស៊ីនណាមួយទេ។ តើអ្នកបានបើកការបង្កប់ស៊ីននៅពេលនាំចេញទេ?",
|
||||
"cannotRestoreFromImage": "មិនអាចស្ដារស៊ីនពីឯកសាររូបភាពនេះបានទេ",
|
||||
"invalidSceneUrl": "មិនអាចនាំចូលស៊ីនពី URL ដែលបានផ្តល់ឱ្យទេ។ វាមានទម្រង់ខុស ឬមិនមានទិន្នន័យ Excalidraw JSON ដែលត្រឹមត្រូវ។",
|
||||
"resetLibrary": "វានឹងសម្អាតបាវទាំងមូល។ តើអ្នកប្រាកដឬអត់?",
|
||||
"removeItemsFromsLibrary": "តើអ្នកប្រាកដថាចង់លុប {{count}} ធាតុចេញពីបណ្ណាល័យទេ?",
|
||||
"invalidEncryptionKey": "សោអ៊ីនគ្រីបត្រូវតែមាន 22 តួអក្សរ។ ការសហការផ្ទាល់ត្រូវបានបិទ។",
|
||||
"collabOfflineWarning": "គ្មានការតភ្ជាប់អ៊ីនធឺណិត។\nការផ្លាស់ប្តូររបស់អ្នកនឹងមិនត្រូវបានរក្សាទុកទេ!"
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "",
|
||||
"imageInsertError": "",
|
||||
"fileTooBig": "",
|
||||
"svgImageInsertError": "",
|
||||
"invalidSVGString": "",
|
||||
"cannotResolveCollabServer": "",
|
||||
"importLibraryError": "",
|
||||
"collabSaveFailed": "",
|
||||
"collabSaveFailed_sizeExceeded": "",
|
||||
"unsupportedFileType": "ប្រភេទឯកសារមិនត្រូវបានគាំទ្រទេ។",
|
||||
"imageInsertError": "មិនអាចបញ្ចូលរូបភាពបានទេ។ សូមព្យាយាមម្តងទៀតនៅពេលក្រោយ……",
|
||||
"fileTooBig": "ឯកសារធំពេក។ ទំហំអតិបរមាដែលអនុញ្ញាតគឺ {{maxSize}}។",
|
||||
"svgImageInsertError": "មិនអាចបញ្ចូលរូបភាព SVG បានទេ។ ស្លាក SVG ហាក់ដូចជាមិនត្រឹមត្រូវ។",
|
||||
"invalidSVGString": "SVG មិនត្រឹមត្រូវ។",
|
||||
"cannotResolveCollabServer": "មិនអាចភ្ជាប់ទៅម៉ាស៊ីនមេសហការផ្ទាល់បានទេ។ សូមផ្ទុកទំព័រឡើងវិញ ហើយព្យាយាមម្តងទៀត។",
|
||||
"importLibraryError": "មិនអាចផ្ទុកបណ្ណាល័យបានទេ។",
|
||||
"collabSaveFailed": "មិនអាចរក្សាទុកទៅម៉ាស៊ីនមេបានទេ។ ប្រសិនបើបញ្ហានៅតែបន្តកើតមាន អ្នកគួរតែរក្សាទុកឯកសាររបស់អ្នកនៅលើកុំព្យូទ័ររបស់អ្នកសិន ដើម្បីធានាថាការងាររបស់អ្នកមិនបាត់បង់។",
|
||||
"collabSaveFailed_sizeExceeded": "មិនអាចរក្សាទុកទៅម៉ាស៊ីនមេបានទេ, ផ្ទាំងបាវហាក់ដូចជាធំពេក។ អ្នកគួរតែរក្សាទុកឯកសាររបស់អ្នកនៅលើកុំព្យូទ័ររបស់អ្នកសិន ដើម្បីធានាថាការងាររបស់អ្នកមិនបាត់បង់។",
|
||||
"brave_measure_text_error": {
|
||||
"line1": "",
|
||||
"line2": "",
|
||||
"line3": "",
|
||||
"line4": ""
|
||||
"line1": "អ្នកហាក់ដូចជាកំពុងប្រើប្រាស់កម្មវិធីរុករកតាមអ៊ីនធឺណិត Brave ជាមួយនឹងការកំណត់ <bold>ការពារស្នាមម្រាមដៃយ៉ាងធ្ងន់ធ្ងរ</bold> ត្រូវបានបើក។",
|
||||
"line2": "វាអាចបណ្តាលឱ្យមានការបំបែក <bold>ធាតុអត្ថបទ</bold> នៅក្នុងគំនូររបស់អ្នក។",
|
||||
"line3": "យើងណែនាំយ៉ាងមុតមាំឱ្យបិទការកំណត់នេះ។ អ្នកអាចអនុវត្តតាម <link>ជំហានទាំងនេះ</link> ដើម្បីបិទការកំណត់នេះ។",
|
||||
"line4": "ប្រសិនបើការបិទការកំណត់នេះមិនបានជួសជុលការបង្ហាញធាតុអត្ថបទទេ សូមដាក់ <issueLink>issue</issueLink> នៅលើ GitHub ឬរាយការណ៍នៅលើ <discordLink>Discord</discordLink> របស់យើង"
|
||||
}
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "",
|
||||
"image": "",
|
||||
"rectangle": "",
|
||||
"diamond": "",
|
||||
"ellipse": "",
|
||||
"arrow": "",
|
||||
"line": "",
|
||||
"freedraw": "",
|
||||
"text": "",
|
||||
"library": "",
|
||||
"lock": "",
|
||||
"penMode": "",
|
||||
"link": "",
|
||||
"eraser": "",
|
||||
"hand": ""
|
||||
"selection": "ការជ្រើសរើស",
|
||||
"image": "បញ្ចូលរូបភាព",
|
||||
"rectangle": "ចតុកោណកែង",
|
||||
"diamond": "ពេជ្រ",
|
||||
"ellipse": "ពងក្រពើ",
|
||||
"arrow": "ព្រួញ",
|
||||
"line": "បន្ទាត់",
|
||||
"freedraw": "គូរ",
|
||||
"text": "អត្ថបទ",
|
||||
"library": "បណ្ណាល័យ",
|
||||
"lock": "រក្សារឧបករណ៍ដែលបានជ្រើសរើសបន្ទាប់ពីគូររួច",
|
||||
"penMode": "របៀបប៊ិច - ជៀសវាងការប៉ះ",
|
||||
"link": "បន្ថែម/ធ្វើបច្ចុប្បន្នភាពតំណភ្ជាប់សម្រាប់រូបរាងដែលបានជ្រើសរើស",
|
||||
"eraser": "ជ័រលុប",
|
||||
"hand": "ដៃ (panning tool)"
|
||||
},
|
||||
"headings": {
|
||||
"canvasActions": "",
|
||||
"selectedShapeActions": "",
|
||||
"shapes": ""
|
||||
"canvasActions": "សកម្មភាពបាវ",
|
||||
"selectedShapeActions": "សកម្មភាពរបស់រាងដែលបានជ្រើសរើស",
|
||||
"shapes": "រាង"
|
||||
},
|
||||
"hints": {
|
||||
"canvasPanning": "",
|
||||
"linearElement": "",
|
||||
"freeDraw": "",
|
||||
"text": "",
|
||||
"text_selected": "",
|
||||
"text_editing": "",
|
||||
"linearElementMulti": "",
|
||||
"lockAngle": "",
|
||||
"resize": "",
|
||||
"resizeImage": "",
|
||||
"rotate": "",
|
||||
"lineEditor_info": "",
|
||||
"lineEditor_pointSelected": "",
|
||||
"lineEditor_nothingSelected": "",
|
||||
"placeImage": "",
|
||||
"publishLibrary": "",
|
||||
"bindTextToElement": "",
|
||||
"deepBoxSelect": "",
|
||||
"eraserRevert": "",
|
||||
"firefox_clipboard_write": ""
|
||||
"canvasPanning": "ដើម្បីផ្លាស់ទីបាវ សូមសង្កត់កង់កណ្ដុរឬគ្រាប់ចុចspacebarខណៈពេលកំពុងអូស ឬប្រើឧបករណ៍ដៃ។",
|
||||
"linearElement": "ចុចដើម្បីបង្កើតចំណុចច្រើន អូសដើម្បីបង្កើតបន្ទាត់មួយ",
|
||||
"freeDraw": "ចុចហើយអូស លែងពេលរួចរាល់",
|
||||
"text": "គន្លឹះ៖ អ្នកក៏អាចបន្ថែមអត្ថបទដោយចុចពីរដងនៅកន្លែងណាមួយដោយប្រើឧបករណ៍ជ្រើសរើស",
|
||||
"text_selected": "ចុចពីរដង ឬចុច ENTER ដើម្បីកែសម្រួលអត្ថបទ",
|
||||
"text_editing": "ចុច Escape ឬ CtrlOrCmd +ENTER ដើម្បីបញ្ចប់ការកែសម្រួល",
|
||||
"linearElementMulti": "ចុចលើចំណុចចុងក្រោយ ឬចុច Esc/Enter ដើម្បីបញ្ចប់",
|
||||
"lockAngle": "អ្នកអាចសង្កត់ Shift ដើម្បីកំណត់មុំ",
|
||||
"resize": "អ្នកអាចសង្កត់ SHIFT ដើម្បីបងំ្ខឲមានសមាមាត្រ ខណៈពេលដែលប្តូរទំហំ\nសង្កត់ ALT ដើម្បីប្តូរទំហំពីកណ្តាល",
|
||||
"resizeImage": "អ្នកអាចប្តូរទំហំរូបភាពដោយសេរីដោយសង្កត់ SHIFT,\nសង្កត់ ALT ដើម្បីប្តូរទំហំពីកណ្តាល",
|
||||
"rotate": "អ្នកអាចសង្កត់ Shift ខណៈពេលកំពុងបង្វិល ដើម្បីកម្រិតមុំ",
|
||||
"lineEditor_info": "សង្កត់ CtrlOrCmd ហើយចុចពីរដង ឬចុច CtrlOrCmd + Enter ដើម្បីកែសម្រួលចំណុច",
|
||||
"lineEditor_pointSelected": "ចុច Delete ដើម្បីលុបចំណុច(ច្រើន)\nCtrlOrCmd+D ដើម្បីចម្លង, ឬអូសដើម្បីផ្លាស់ទី",
|
||||
"lineEditor_nothingSelected": "ជ្រើសរើសចំណុចដែលត្រូវកែសម្រួល (សង្កត់ SHIFT ដើម្បីជ្រើសរើសច្រើនចំណុច)\nឬ សង្កត់ Alt ហើយចុចដើម្បីបន្ថែមចំណុចថ្មី។",
|
||||
"placeImage": "ចុចដើម្បីដាក់រូបភាព ឬចុចហើយអូសដើម្បីកំណត់ទំហំរបស់រូបភាពដោយដៃ",
|
||||
"publishLibrary": "បោះពុម្ពផ្សាយបណ្ណាល័យផ្ទាល់ខ្លួនរបស់អ្នក",
|
||||
"bindTextToElement": "ចុច Enter ដើម្បីបន្ថែមអត្ថបទ",
|
||||
"deepBoxSelect": "សង្កត់ CtrlOrCmd ដើម្បីជ្រើសរើសយ៉ាងជ្រៅ និងជៀសវាងការអូស",
|
||||
"eraserRevert": "សង្កត់ Alt ដើម្បីដកការជ្រើសរើសធាតុដែលត្រូវបានសម្គាល់សម្រាប់ការលុប",
|
||||
"firefox_clipboard_write": "បើកមុខងារនេះដោយកំណត់ទង់ \"dom.events.asyncClipboard.clipboardItem\" ទៅ \"true\" \nដើម្បីផ្លាស់ប្តូរទង់កម្មវិធីរុករកនៅក្នុង Firefox សូមចូលទៅកាន់ទំព័រ \"about:config\"។"
|
||||
},
|
||||
"canvasError": {
|
||||
"cannotShowPreview": "",
|
||||
"canvasTooBig": "",
|
||||
"canvasTooBigTip": ""
|
||||
"cannotShowPreview": "មិនអាចបង្ហាញការមើលជាមុនបាន",
|
||||
"canvasTooBig": "បាវអាចមានទំហំធំពេក។",
|
||||
"canvasTooBigTip": "គន្លឹះ៖ ព្យាយាមផ្លាស់ទីធាតុដែលឆ្ងាយបំផុតឱ្យទៅជិតគ្នាបន្តិច។"
|
||||
},
|
||||
"errorSplash": {
|
||||
"headingMain": "",
|
||||
"clearCanvasMessage": "",
|
||||
"clearCanvasCaveat": "",
|
||||
"trackedToSentry": "",
|
||||
"openIssueMessage": "",
|
||||
"sceneContent": ""
|
||||
"headingMain": "បានជួបប្រទះកំហុសមួយ។ សូមព្យាយាម <button>ផ្ទុកទំព័រឡើងវិញ</button>។",
|
||||
"clearCanvasMessage": "ប្រសិនបើការផ្ទុកឡើងវិញមិនអាចដោះស្រាយកំហុសបានទេ សូមសាកល្បង <button>សម្អាតបាវ។</button>",
|
||||
"clearCanvasCaveat": " នេះនឹងបណ្តាលឱ្យបាត់បង់ការងារ ",
|
||||
"trackedToSentry": "កំហុសជាមួយលេខសម្គាល់ {{eventId}} ត្រូវបានតាមដាននៅលើប្រព័ន្ធរបស់យើង។",
|
||||
"openIssueMessage": "យើងមានការប្រុងប្រយ័ត្នខ្លាំងណាស់ក្នុងការមិនបញ្ចូលព័ត៌មានរបស់ស៊ីនរបស់អ្នកទៅលើកំហុស។ ប្រសិនបើស៊ីនរបស់អ្នកមិនមានលក្ខណៈឯកជនទេ សូមពិចារណាបន្តទៅកាន់ <button>កម្មវិធីតាមដានកំហុសរបស់យើង។</button> សូមបញ្ចូលព័ត៌មានខាងក្រោមដោយចម្លង និងបិទភ្ជាប់វាទៅក្នុងបញ្ហារបស់ GitHub។",
|
||||
"sceneContent": "មាតិកាបាវ៖"
|
||||
},
|
||||
"roomDialog": {
|
||||
"desc_intro": "",
|
||||
"desc_privacy": "",
|
||||
"button_startSession": "",
|
||||
"button_stopSession": "",
|
||||
"desc_inProgressIntro": "",
|
||||
"desc_shareLink": "",
|
||||
"desc_exitSession": "",
|
||||
"shareTitle": ""
|
||||
"desc_intro": "អ្នកអាចអញ្ជើញអ្នកដទៃឱ្យសហការជាមួយអ្នកនៅលើស៊ីនបច្ចុប្បន្ន។",
|
||||
"desc_privacy": "កុំបារម្ភ វគ្គប្រើការអ៊ិនគ្រីបពីចុងដល់ចប់ ដូច្នេះអ្វីដែលអ្នកគូរនឹងនៅតែជាឯកជន។ សូម្បីតែម៉ាស៊ីនមេរបស់យើងក៏នឹងមិនអាចមើលឃើញអ្វីដែលអ្នកកំពុងធ្វើដែរ។",
|
||||
"button_startSession": "ចាប់ផ្តើមវគ្គ",
|
||||
"button_stopSession": "បញ្ឈប់វគ្គ",
|
||||
"desc_inProgressIntro": "វគ្គសហការផ្ទាល់ឥឡូវនេះកំពុងដំណើរការ។",
|
||||
"desc_shareLink": "ចែករំលែកតំណនេះជាមួយអ្នកដែលអ្នកចង់សហការជាមួយ៖",
|
||||
"desc_exitSession": "ការបញ្ឈប់វគ្គនេះនឹងផ្តាច់អ្នកចេញពីបន្ទប់ ប៉ុន្តែអ្នកនឹងនៅតែអាចបន្តប្រើបាវនៅលើកុំព្យូទ័ររបស់អ្នក។ សូមចំណាំថាវានឹងមិនប៉ះពាល់ដល់អ្នកប្រើប្រាស់ផ្សេងទៀតទេ ហើយពួកគេនឹងនៅតែអាចបន្តសហការលើកំណែរបស់ពួកគេ។",
|
||||
"shareTitle": "ចូលរួមវគ្គសហការផ្ទាល់នៅលើ Excalidraw"
|
||||
},
|
||||
"errorDialog": {
|
||||
"title": ""
|
||||
"title": "មានកំហុស"
|
||||
},
|
||||
"exportDialog": {
|
||||
"disk_title": "",
|
||||
"disk_details": "",
|
||||
"disk_button": "",
|
||||
"link_title": "",
|
||||
"link_details": "",
|
||||
"link_button": "",
|
||||
"excalidrawplus_description": "",
|
||||
"excalidrawplus_button": "",
|
||||
"excalidrawplus_exportError": ""
|
||||
"disk_title": "រក្សាទុកទៅថាស",
|
||||
"disk_details": "នាំចេញទិន្នន័យរបស់ស៊ីនជាឯកសារដែលអ្នកអាចនាំចូលនៅពេលក្រោយ។",
|
||||
"disk_button": "រក្សាទុកក្នុងឯកសារ",
|
||||
"link_title": "តំណដែលអាចចែករំលែកបាន",
|
||||
"link_details": "នាំចេញជាតំណបានតែមើលឬអាន។",
|
||||
"link_button": "នាំចេញជាតំណ",
|
||||
"excalidrawplus_description": "រក្សាទុកស៊ីនទៅកន្លែងធ្វើការ Excalidraw+ របស់អ្នក។",
|
||||
"excalidrawplus_button": "នាំចេញ",
|
||||
"excalidrawplus_exportError": "មិនអាចនាំចេញទៅ Excalidraw+ បានទេនៅពេលនេះ..."
|
||||
},
|
||||
"helpDialog": {
|
||||
"blog": "",
|
||||
"click": "",
|
||||
"deepSelect": "",
|
||||
"deepBoxSelect": "",
|
||||
"curvedArrow": "",
|
||||
"curvedLine": "",
|
||||
"documentation": "",
|
||||
"doubleClick": "",
|
||||
"drag": "",
|
||||
"editor": "",
|
||||
"editLineArrowPoints": "",
|
||||
"editText": "",
|
||||
"github": "",
|
||||
"howto": "",
|
||||
"or": "",
|
||||
"preventBinding": "",
|
||||
"tools": "",
|
||||
"shortcuts": "",
|
||||
"textFinish": "",
|
||||
"textNewLine": "",
|
||||
"title": "",
|
||||
"view": "",
|
||||
"zoomToFit": "",
|
||||
"zoomToSelection": "",
|
||||
"toggleElementLock": "",
|
||||
"movePageUpDown": "",
|
||||
"movePageLeftRight": ""
|
||||
"blog": "អានប្លក់របស់យើង",
|
||||
"click": "ចុច",
|
||||
"deepSelect": "ការជ្រើសរើសជាក្រុម",
|
||||
"deepBoxSelect": "ជ្រើសរើសជាក្រុម និង ជៀសវាងការអូសទាញផ្លាស់ទី",
|
||||
"curvedArrow": "ព្រួញកោង",
|
||||
"curvedLine": "ព្រួញកោង",
|
||||
"documentation": "ឯកសារ",
|
||||
"doubleClick": "ចុចពីរដង",
|
||||
"drag": "អូស",
|
||||
"editor": "កម្មវិធីនិពន្ធ",
|
||||
"editLineArrowPoints": "កែសម្រួលចំណុចនៃបន្ទាត់ ឬព្រួញ",
|
||||
"editText": "បន្ថែម ឬកែសម្រួលអត្ថបទ",
|
||||
"github": "រកឃើញបញ្ហា? ដាក់ស្នើ",
|
||||
"howto": "ឯកសារជំនួយ",
|
||||
"or": "ឬ",
|
||||
"preventBinding": "ទប់ស្កាត់ការចងព្រួញ",
|
||||
"tools": "ឧបករណ៍",
|
||||
"shortcuts": "ផ្លូវកាត់ក្តារចុច",
|
||||
"textFinish": "បញ្ចប់ការកែសម្រួល (កម្មវិធីនិពន្ធអត្ថបទ)",
|
||||
"textNewLine": "ចុះបន្ទាត់ (កម្មវិធីនិពន្ធអត្ថបទ)",
|
||||
"title": "ជំនួយ",
|
||||
"view": "បង្ហាញ",
|
||||
"zoomToFit": "ធ្វើមាត្រដ្ឋានឱ្យសមនឹងធាតុទាំងអស់។",
|
||||
"zoomToSelection": "ពង្រីកទៅការជ្រើសរើស",
|
||||
"toggleElementLock": "ចាក់សោ/ដោះសោការជ្រើសរើស",
|
||||
"movePageUpDown": "ផ្លាស់ទីទំព័រឡើងលើ/ចុះក្រោម",
|
||||
"movePageLeftRight": "ផ្លាស់ទីទំព័រទៅឆ្វេង/ស្ដាំ"
|
||||
},
|
||||
"clearCanvasDialog": {
|
||||
"title": ""
|
||||
"title": "សម្អាតបាវ"
|
||||
},
|
||||
"publishDialog": {
|
||||
"title": "",
|
||||
"itemName": "",
|
||||
"authorName": "",
|
||||
"githubUsername": "",
|
||||
"twitterUsername": "",
|
||||
"libraryName": "",
|
||||
"libraryDesc": "",
|
||||
"website": "",
|
||||
"title": "បោះពុម្ពបណ្ណាល័យ",
|
||||
"itemName": "ឈ្មោះធាតុ",
|
||||
"authorName": "ឈ្មោះអ្នកនិពន្ធ",
|
||||
"githubUsername": "ឈ្មោះអ្នកប្រើ GitHub",
|
||||
"twitterUsername": "ឈ្មោះអ្នកប្រើ Twitter",
|
||||
"libraryName": "ឈ្មោះបណ្ណាល័យ",
|
||||
"libraryDesc": "ការពិពណ៌នាអំពីបណ្ណាល័យ",
|
||||
"website": "គេហទំព័រ",
|
||||
"placeholder": {
|
||||
"authorName": "",
|
||||
"libraryName": "",
|
||||
"libraryDesc": "",
|
||||
"githubHandle": "",
|
||||
"twitterHandle": "",
|
||||
"website": ""
|
||||
"authorName": "ឈ្មោះរបស់អ្នក ឬឈ្មោះអ្នកប្រើប្រាស់",
|
||||
"libraryName": "ឈ្មោះបណ្ណាល័យរបស់អ្នក",
|
||||
"libraryDesc": "ការពិពណ៌នាអំពីបណ្ណាល័យរបស់អ្នក នឹងអនុញ្ញាតឱ្យអ្នកផ្សេងយល់ពីការប្រើប្រាស់របស់វា។",
|
||||
"githubHandle": "ឈ្មោះអ្នកប្រើ GitHub (ជាជម្រើស) ដូច្នេះអ្នកអាចកែសម្រួលបណ្ណាល័យបាននៅពេលដាក់ស្នើសម្រាប់ការពិនិត្យ",
|
||||
"twitterHandle": "ឈ្មោះអ្នកប្រើប្រាស់ Twitter (ជាជម្រើស) ដូច្នេះយើងដឹងថាអ្នកណាដែលត្រូវផ្តល់ក្រេឌីតនៅពេលផ្សព្វផ្សាយតាម Twitter",
|
||||
"website": "ភ្ជាប់ទៅគេហទំព័រផ្ទាល់ខ្លួនរបស់អ្នក ឬគេហទំព័រផ្សេងទៀត (ជាជម្រើស)"
|
||||
},
|
||||
"errors": {
|
||||
"required": "",
|
||||
"website": ""
|
||||
"required": "ត្រូវបានទាមទារ",
|
||||
"website": "សូមវាយបញ្ចូល URL ដែលត្រឹមត្រូវ"
|
||||
},
|
||||
"noteDescription": "",
|
||||
"noteGuidelines": "",
|
||||
"noteLicense": "",
|
||||
"noteItems": "",
|
||||
"atleastOneLibItem": "",
|
||||
"republishWarning": ""
|
||||
"noteDescription": "ដាក់ស្នើបណ្ណាល័យរបស់អ្នកដើម្បីដាក់បញ្ចូលក្នុង <link>ឃ្លាំងបណ្ណាល័យសាធារណៈ</link> សម្រាប់ឲ្យអ្នកផ្សេងប្រើក្នុងគំនូររបស់ពួកគេ។",
|
||||
"noteGuidelines": "បណ្ណាល័យត្រូវតែអនុម័តដោយដៃជាមុនសិន។ សូមអាន <link>ការណែនាំ</link> មុនពេលដាក់ស្នើ។ ការប្រាស្រ័យទាក់ទងជាបន្តបន្ទាប់ និងការផ្លាស់ប្តូរបណ្ណាល័យទាមទារឱ្យអ្នកមានគណនី GitHub ប៉ុន្តែមិនត្រូវបានទាមទារយ៉ាងតឹងរ៉ឹងទេ។",
|
||||
"noteLicense": "តាមរយៈការដាក់ស្នើ អ្នកយល់ព្រមថាបណ្ណាល័យនឹងត្រូវបានបោះពុម្ពផ្សាយក្រោម <link>អាជ្ញាប័ណ្ណ MIT</link> ដែលមានន័យយ៉ាងខ្លី អ្នកណាក៏អាចប្រើប្រាស់វាបានដោយគ្មានការរឹតត្បិត។",
|
||||
"noteItems": "ធាតុនីមួយៗនៅក្នុងបណ្ណាល័យត្រូវតែមានឈ្មោះផ្ទាល់ខ្លួនដើម្បីយើងអាចត្រងវាបាន។ ធាតុខាងក្រោមនឹងត្រូវបានរួមបញ្ចូល:",
|
||||
"atleastOneLibItem": "សូមជ្រើសរើសយ៉ាងហោចណាស់ធាតុបណ្ណាល័យមួយដើម្បីចាប់ផ្តើម",
|
||||
"republishWarning": "ចំណាំ៖ ធាតុដែលត្រូវបានជ្រើសរើសមួយចំនួនត្រូវបានសម្គាល់ថាបានបោះពុម្ព/បញ្ជូនរួចរាល់ហើយ។ អ្នកគួរតែបញ្ជូនធាតុឡើងវិញនៅពេលដែលធ្វើបច្ចុប្បន្នភាពបណ្ណាល័យដែលមានស្រាប់ ឬការបញ្ជូន។"
|
||||
},
|
||||
"publishSuccessDialog": {
|
||||
"title": "",
|
||||
"content": ""
|
||||
"title": "បណ្ណាល័យត្រូវបានដាក់ស្នើ",
|
||||
"content": "សូមអរគុណ {{authorName}}។ បណ្ណាល័យរបស់អ្នកត្រូវបានបញ្ជូនមកពិនិត្យ។ សូមចុច <link>ទីនេះ</link> ដើម្បីតាមដានស្ថានភាពនៃការដាក់ស្នើនេះ។"
|
||||
},
|
||||
"confirmDialog": {
|
||||
"resetLibrary": "",
|
||||
"removeItemsFromLib": ""
|
||||
"resetLibrary": "កំណត់បណ្ណាល័យឡើងវិញ",
|
||||
"removeItemsFromLib": "លុបធាតុដែលបានជ្រើសរើសចេញពីបណ្ណាល័យ"
|
||||
},
|
||||
"imageExportDialog": {
|
||||
"header": "",
|
||||
"label": {
|
||||
"withBackground": "",
|
||||
"onlySelected": "",
|
||||
"darkMode": "",
|
||||
"embedScene": "",
|
||||
"scale": "",
|
||||
"padding": ""
|
||||
},
|
||||
"tooltip": {
|
||||
"embedScene": ""
|
||||
},
|
||||
"title": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
},
|
||||
"button": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
}
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "",
|
||||
"link": ""
|
||||
"tooltip": "គំនូររបស់អ្នកត្រូវបានអ៊ិនគ្រីបពីចុងដល់ចប់ ដូច្នេះម៉ាស៊ីនមេរបស់ Excalidraw នឹងមិនឃើញពួកវាទេ។",
|
||||
"link": "ប្លក់ផុសលើការអ៊ិនគ្រីបពីចុងដល់ចុងក្នុង Excalidraw"
|
||||
},
|
||||
"stats": {
|
||||
"angle": "",
|
||||
"element": "",
|
||||
"elements": "",
|
||||
"height": "",
|
||||
"scene": "",
|
||||
"selected": "",
|
||||
"storage": "",
|
||||
"title": "",
|
||||
"total": "",
|
||||
"version": "",
|
||||
"versionCopy": "",
|
||||
"versionNotAvailable": "",
|
||||
"width": ""
|
||||
"angle": "មុំ",
|
||||
"element": "ធាតុ",
|
||||
"elements": "ធាតុច្រើន",
|
||||
"height": "កម្ពស់",
|
||||
"scene": "ស៊ីន",
|
||||
"selected": "បានជ្រើសរើស",
|
||||
"storage": "ការផ្ទុក",
|
||||
"title": "ស្ថិតិសម្រាប់ nerds",
|
||||
"total": "សរុប",
|
||||
"version": "ជំនាន់:",
|
||||
"versionCopy": "ចុចដើម្បីចម្លង",
|
||||
"versionNotAvailable": "កំណែមិនអាចប្រើបាន",
|
||||
"width": "ទទឹង"
|
||||
},
|
||||
"toast": {
|
||||
"addedToLibrary": "",
|
||||
"copyStyles": "",
|
||||
"copyToClipboard": "",
|
||||
"copyToClipboardAsPng": "",
|
||||
"fileSaved": "",
|
||||
"fileSavedToFilename": "",
|
||||
"canvas": "",
|
||||
"selection": "",
|
||||
"pasteAsSingleElement": ""
|
||||
"addedToLibrary": "បានបន្ថែមទៅបណ្ណាល័យ",
|
||||
"copyStyles": "រចនាប័ទ្មត្រូវបានចម្លង។",
|
||||
"copyToClipboard": "បានចម្លងទៅក្ដារតម្បៀតខ្ទាស់។",
|
||||
"copyToClipboardAsPng": "បានចម្លង {{exportSelection}} ទៅក្ដារតម្បៀតខ្ទាស់ជា PNG\n({{exportColorScheme}})",
|
||||
"fileSaved": "ឯកសារត្រូវបានរក្សាទុក។",
|
||||
"fileSavedToFilename": "បានរក្សាទុកនៅក្នុង {filename}",
|
||||
"canvas": "តំបន់គំនូរ",
|
||||
"selection": "ការជ្រើសរើស",
|
||||
"pasteAsSingleElement": "ប្រើ {{shortcut}} ដើម្បីបិទភ្ជាប់ជាធាតុតែមួយ,\nឬបិទភ្ជាប់ទៅក្នុងកម្មវិធីនិពន្ធអត្ថបទដែលមានស្រាប់"
|
||||
},
|
||||
"colors": {
|
||||
"transparent": "ថ្លាមើលធ្លុះ",
|
||||
"black": "",
|
||||
"white": "",
|
||||
"red": "",
|
||||
"pink": "",
|
||||
"grape": "",
|
||||
"violet": "",
|
||||
"gray": "",
|
||||
"blue": "",
|
||||
"cyan": "",
|
||||
"teal": "",
|
||||
"green": "",
|
||||
"yellow": "",
|
||||
"orange": "",
|
||||
"bronze": ""
|
||||
"black": "ពណ៍ខ្មៅ",
|
||||
"white": "ពណ៌ស",
|
||||
"red": "ពណ៌ក្រហម",
|
||||
"pink": "ពណ៌ផ្កាឈូក",
|
||||
"grape": "ពណ៌ទំពាំងបាយជូរ",
|
||||
"violet": "ពណ៌ស្វាយ",
|
||||
"gray": "ពណ៌ប្រផេះ",
|
||||
"blue": "ពណ៌ខៀវ",
|
||||
"cyan": "ពណ៌ផ្ទៃមេឃ",
|
||||
"teal": "ពណ៌ខៀវបៃតង",
|
||||
"green": "ពណ៌បៃតង",
|
||||
"yellow": "ពណ៌លឿង",
|
||||
"orange": "ពណ៌ទឹកក្រូច",
|
||||
"bronze": "ពណ៌សំរិទ្ធ"
|
||||
},
|
||||
"welcomeScreen": {
|
||||
"app": {
|
||||
|
@ -424,10 +440,10 @@
|
|||
}
|
||||
},
|
||||
"colorPicker": {
|
||||
"mostUsedCustomColors": "",
|
||||
"colors": "",
|
||||
"shades": "",
|
||||
"hexCode": "",
|
||||
"noShades": ""
|
||||
"mostUsedCustomColors": "ពណ៌ផ្ទាល់ខ្លួនដែលប្រើច្រើនបំផុត",
|
||||
"colors": "ពណ៌",
|
||||
"shades": "ស្រមោល",
|
||||
"hexCode": "លេខកូដ hex",
|
||||
"noShades": "មិនមានស្រមោលសម្រាប់ពណ៌នេះទេ"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,10 +40,6 @@
|
|||
"arrowhead_triangle": "삼각형",
|
||||
"fontSize": "글자 크기",
|
||||
"fontFamily": "글꼴",
|
||||
"onlySelected": "선택한 항목만",
|
||||
"withBackground": "배경",
|
||||
"exportEmbedScene": "화면을 담기",
|
||||
"exportEmbedScene_details": "화면 정보가 내보내는 PNG/SVG 파일에 저장되어 이후에 파일에서 화면을 복구할 수 있습니다. 파일 크기가 증가합니다.",
|
||||
"addWatermark": "\"Made with Excalidraw\" 추가",
|
||||
"handDrawn": "손글씨",
|
||||
"normal": "일반",
|
||||
|
@ -100,7 +96,6 @@
|
|||
"flipHorizontal": "좌우반전",
|
||||
"flipVertical": "상하반전",
|
||||
"viewMode": "보기 모드",
|
||||
"toggleExportColorScheme": "내보내기의 색상 테마 전환하기",
|
||||
"share": "공유",
|
||||
"showStroke": "윤곽선 색상 선택기 열기",
|
||||
"showBackground": "배경 색상 선택기 열기",
|
||||
|
@ -128,7 +123,8 @@
|
|||
"unlockAll": "모두 잠금 해제"
|
||||
},
|
||||
"statusPublished": "게시됨",
|
||||
"sidebarLock": "사이드바 유지"
|
||||
"sidebarLock": "사이드바 유지",
|
||||
"eyeDropper": ""
|
||||
},
|
||||
"library": {
|
||||
"noItems": "추가된 아이템 없음",
|
||||
|
@ -140,11 +136,7 @@
|
|||
"exportJSON": "파일로 내보내기",
|
||||
"exportImage": "이미지 내보내기",
|
||||
"export": "다른 이름으로 저장...",
|
||||
"exportToPng": "PNG로 내보내기",
|
||||
"exportToSvg": "SVG로 내보내기",
|
||||
"copyToClipboard": "클립보드로 복사",
|
||||
"copyPngToClipboard": "클립보드로 PNG 이미지 복사",
|
||||
"scale": "크기",
|
||||
"save": "현재 파일에 저장",
|
||||
"saveAs": "다른 이름으로 저장",
|
||||
"load": "열기",
|
||||
|
@ -363,6 +355,30 @@
|
|||
"resetLibrary": "라이브러리 리셋",
|
||||
"removeItemsFromLib": "선택한 항목을 라이브러리에서 제거"
|
||||
},
|
||||
"imageExportDialog": {
|
||||
"header": "이미지 내보내기",
|
||||
"label": {
|
||||
"withBackground": "배경",
|
||||
"onlySelected": "선택한 항목만",
|
||||
"darkMode": "다크 모드",
|
||||
"embedScene": "화면을 담기",
|
||||
"scale": "크기",
|
||||
"padding": "여백"
|
||||
},
|
||||
"tooltip": {
|
||||
"embedScene": "화면 정보가 내보내는 PNG/SVG 파일에 저장되어 이후에 파일에서 화면을 복구할 수 있습니다. 파일 크기가 증가합니다."
|
||||
},
|
||||
"title": {
|
||||
"exportToPng": "PNG로 내보내기",
|
||||
"exportToSvg": "SVG로 내보내기",
|
||||
"copyPngToClipboard": "클립보드로 PNG 복사"
|
||||
},
|
||||
"button": {
|
||||
"exportToPng": "PNG",
|
||||
"exportToSvg": "SVG",
|
||||
"copyPngToClipboard": "클립보드로 복사"
|
||||
}
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "그림은 종단 간 암호화되므로 Excalidraw의 서버는 절대로 내용을 알 수 없습니다.",
|
||||
"link": "Excalidraw의 종단 간 암호화에 대한 블로그 포스트"
|
||||
|
@ -395,20 +411,20 @@
|
|||
},
|
||||
"colors": {
|
||||
"transparent": "투명",
|
||||
"black": "",
|
||||
"white": "",
|
||||
"red": "",
|
||||
"pink": "",
|
||||
"grape": "",
|
||||
"violet": "",
|
||||
"gray": "",
|
||||
"blue": "",
|
||||
"cyan": "",
|
||||
"teal": "",
|
||||
"green": "",
|
||||
"yellow": "",
|
||||
"orange": "",
|
||||
"bronze": ""
|
||||
"black": "블랙",
|
||||
"white": "화이트",
|
||||
"red": "레드",
|
||||
"pink": "핑크",
|
||||
"grape": "그레이프",
|
||||
"violet": "바이올렛",
|
||||
"gray": "그레이",
|
||||
"blue": "블루",
|
||||
"cyan": "시안",
|
||||
"teal": "틸",
|
||||
"green": "그린",
|
||||
"yellow": "옐로우",
|
||||
"orange": "오렌지",
|
||||
"bronze": "브론즈"
|
||||
},
|
||||
"welcomeScreen": {
|
||||
"app": {
|
||||
|
@ -424,10 +440,10 @@
|
|||
}
|
||||
},
|
||||
"colorPicker": {
|
||||
"mostUsedCustomColors": "",
|
||||
"colors": "",
|
||||
"shades": "",
|
||||
"hexCode": "",
|
||||
"noShades": ""
|
||||
"mostUsedCustomColors": "많이 쓰이는 사용자 색상들",
|
||||
"colors": "색상",
|
||||
"shades": "색조",
|
||||
"hexCode": "Hex 코드",
|
||||
"noShades": "사용할 수 있는 색조가 없음"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,10 +40,6 @@
|
|||
"arrowhead_triangle": "سێگۆشە",
|
||||
"fontSize": "قەبارەی فۆنت",
|
||||
"fontFamily": "خێزانی فۆنت",
|
||||
"onlySelected": "تەنها دیاریکراوەکان",
|
||||
"withBackground": "باکگراوند",
|
||||
"exportEmbedScene": "ئیمبێدکردنی دیمەنەکە",
|
||||
"exportEmbedScene_details": "هەنداردەکراو بۆ ئەوەی دیمەنەکە بتوانرێت بگەڕێنرێتەوە لێی (PNG/SVG) داتای دیمەنەکە هەڵدەگیرێت وەکو فایلی\nقەبارەی فایلە هەناردەکراوەکان زیاد دەکات.",
|
||||
"addWatermark": "زیادبکە \"Made with Excalidraw\"",
|
||||
"handDrawn": "دەست کێشراو",
|
||||
"normal": "ئاسایی",
|
||||
|
@ -75,10 +71,10 @@
|
|||
"language": "زمان",
|
||||
"liveCollaboration": "هاوکاریکردنی زیندو...",
|
||||
"duplicateSelection": "لەبەرگرتنەوە",
|
||||
"untitled": "Untitled",
|
||||
"untitled": "بێ-ناو",
|
||||
"name": "ناو",
|
||||
"yourName": "ناوەکەت",
|
||||
"madeWithExcalidraw": "Made with Excalidraw",
|
||||
"madeWithExcalidraw": "دروستکراوە بە Excalidraw",
|
||||
"group": "دیاریکردنی گروپ",
|
||||
"ungroup": "گروپی دیاریکراوەکان لابەرە",
|
||||
"collaborators": "هاوکارەکان",
|
||||
|
@ -100,7 +96,6 @@
|
|||
"flipHorizontal": "هەڵگەڕانەوەی ئاسۆیی",
|
||||
"flipVertical": "هەڵگەڕانەوەی ستونی",
|
||||
"viewMode": "دۆخی بینین",
|
||||
"toggleExportColorScheme": "گۆڕینی بارکردنی هێلکاری ڕەنگەکان",
|
||||
"share": "هاوبەشی پێکردن",
|
||||
"showStroke": "ڕەنگهەڵگری هێڵکار نیشانبدە",
|
||||
"showBackground": "ڕەنگهەڵگری باکگراوند نیشانبدە",
|
||||
|
@ -128,7 +123,8 @@
|
|||
"unlockAll": "کردنەوەی قفلی هەمووی"
|
||||
},
|
||||
"statusPublished": "بڵاوکراوەتەوە",
|
||||
"sidebarLock": "هێشتنەوەی شریتی لا بە کراوەیی"
|
||||
"sidebarLock": "هێشتنەوەی شریتی لا بە کراوەیی",
|
||||
"eyeDropper": ""
|
||||
},
|
||||
"library": {
|
||||
"noItems": "هێشتا هیچ بڕگەیەک زیاد نەکراوە...",
|
||||
|
@ -140,11 +136,7 @@
|
|||
"exportJSON": "هەناردەکردن بۆ فایل",
|
||||
"exportImage": "وێنە هەناردە بکە...",
|
||||
"export": "پاشەکەوت بکە بۆ...",
|
||||
"exportToPng": "هەناردەکردن بۆ PNG",
|
||||
"exportToSvg": "هەناردەکردن بۆ SVG",
|
||||
"copyToClipboard": "لهبهری بگرهوه بۆ تهختهنووس",
|
||||
"copyPngToClipboard": "لەبەرگرتنەوەی PNG بۆ تەختەنوس",
|
||||
"scale": "پێوەر",
|
||||
"save": "پاشەکەوت بکە بۆ فایلی بەردەست",
|
||||
"saveAs": "پاشەکەوتکردن وەک",
|
||||
"load": "بکەرەوە",
|
||||
|
@ -363,6 +355,30 @@
|
|||
"resetLibrary": "ڕێکخستنەوەی کتێبخانە",
|
||||
"removeItemsFromLib": "لابردنی ئایتمە دیاریکراوەکان لە کتێبخانە"
|
||||
},
|
||||
"imageExportDialog": {
|
||||
"header": "",
|
||||
"label": {
|
||||
"withBackground": "",
|
||||
"onlySelected": "",
|
||||
"darkMode": "",
|
||||
"embedScene": "",
|
||||
"scale": "",
|
||||
"padding": ""
|
||||
},
|
||||
"tooltip": {
|
||||
"embedScene": ""
|
||||
},
|
||||
"title": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
},
|
||||
"button": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
}
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "وێنەکێشانەکانت لە کۆتاییەوە بۆ کۆتایی کۆد کراون بۆیە سێرڤەرەکانی ئێکسکالیدرا هەرگیز نایانبینن.",
|
||||
"link": "بلۆگ پۆست لەسەر کۆدکردنی کۆتای بۆ کۆتای لە ئێکسکالیدرەو"
|
||||
|
|
|
@ -40,10 +40,6 @@
|
|||
"arrowhead_triangle": "Trikampis",
|
||||
"fontSize": "Šrifto dydis",
|
||||
"fontFamily": "Šriftas",
|
||||
"onlySelected": "Tik pasirinktus",
|
||||
"withBackground": "Fonas",
|
||||
"exportEmbedScene": "Įterpti sceną",
|
||||
"exportEmbedScene_details": "Scenos duomenys bus išsaugoti eksportuotame PNG/SVG faile taip, jog vėliau scena galėtu būti iš jo atkurta.\nTai padydins eksportuoto failo dydį.",
|
||||
"addWatermark": "Sukurta su Excalidraw",
|
||||
"handDrawn": "Ranka rašytas",
|
||||
"normal": "Normalus",
|
||||
|
@ -100,7 +96,6 @@
|
|||
"flipHorizontal": "Apversti horizontaliai",
|
||||
"flipVertical": "Apversti vertikaliai",
|
||||
"viewMode": "",
|
||||
"toggleExportColorScheme": "",
|
||||
"share": "Dalintis",
|
||||
"showStroke": "",
|
||||
"showBackground": "",
|
||||
|
@ -128,7 +123,8 @@
|
|||
"unlockAll": ""
|
||||
},
|
||||
"statusPublished": "",
|
||||
"sidebarLock": ""
|
||||
"sidebarLock": "",
|
||||
"eyeDropper": ""
|
||||
},
|
||||
"library": {
|
||||
"noItems": "",
|
||||
|
@ -140,11 +136,7 @@
|
|||
"exportJSON": "Eksportuoti į failą",
|
||||
"exportImage": "",
|
||||
"export": "",
|
||||
"exportToPng": "Eksportuoti į PNG",
|
||||
"exportToSvg": "Eksportuoti į SVG",
|
||||
"copyToClipboard": "Kopijuoti į iškarpinę",
|
||||
"copyPngToClipboard": "Kopijuoti PNG į iškarpinę",
|
||||
"scale": "",
|
||||
"save": "",
|
||||
"saveAs": "Išsaugoti kaip",
|
||||
"load": "",
|
||||
|
@ -363,6 +355,30 @@
|
|||
"resetLibrary": "Atstatyti biblioteką",
|
||||
"removeItemsFromLib": "Pašalinti pasirinktus elementus iš bibliotekos"
|
||||
},
|
||||
"imageExportDialog": {
|
||||
"header": "",
|
||||
"label": {
|
||||
"withBackground": "",
|
||||
"onlySelected": "",
|
||||
"darkMode": "",
|
||||
"embedScene": "",
|
||||
"scale": "",
|
||||
"padding": ""
|
||||
},
|
||||
"tooltip": {
|
||||
"embedScene": ""
|
||||
},
|
||||
"title": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
},
|
||||
"button": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
}
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "",
|
||||
"link": ""
|
||||
|
|
|
@ -40,10 +40,6 @@
|
|||
"arrowhead_triangle": "Trijstūris",
|
||||
"fontSize": "Teksta lielums",
|
||||
"fontFamily": "Fontu saime",
|
||||
"onlySelected": "Tikai atlasītais",
|
||||
"withBackground": "Fons",
|
||||
"exportEmbedScene": "Iegult ainu",
|
||||
"exportEmbedScene_details": "Ainas dati tiks iekļauti saglabātajā PNG/SVG datnē, lai no tās būtu iespējams ainu atgūt. Tas palielinās datnes izmēru.",
|
||||
"addWatermark": "Pievienot \"Radīts ar Excalidraw\"",
|
||||
"handDrawn": "Rokraksts",
|
||||
"normal": "Parasts",
|
||||
|
@ -100,7 +96,6 @@
|
|||
"flipHorizontal": "Apmest horizontāli",
|
||||
"flipVertical": "Apmest vertikāli",
|
||||
"viewMode": "Skata režīms",
|
||||
"toggleExportColorScheme": "Pārslēgt eksporta krāsu tēmu",
|
||||
"share": "Kopīgot",
|
||||
"showStroke": "Rādīt svītras krāsas atlasītāju",
|
||||
"showBackground": "Rādīt fona krāsas atlasītāju",
|
||||
|
@ -128,7 +123,8 @@
|
|||
"unlockAll": "Atbrīvot visu"
|
||||
},
|
||||
"statusPublished": "Publicēts",
|
||||
"sidebarLock": "Paturēt atvērtu sānjoslu"
|
||||
"sidebarLock": "Paturēt atvērtu sānjoslu",
|
||||
"eyeDropper": ""
|
||||
},
|
||||
"library": {
|
||||
"noItems": "Neviena vienība vēl nav pievienota...",
|
||||
|
@ -140,11 +136,7 @@
|
|||
"exportJSON": "Eksportēt kā failu",
|
||||
"exportImage": "Eksportēt attēlu...",
|
||||
"export": "Saglabāt uz...",
|
||||
"exportToPng": "Eksportēt kā PNG",
|
||||
"exportToSvg": "Eksportēt kā SVG",
|
||||
"copyToClipboard": "Kopēt starpliktuvē",
|
||||
"copyPngToClipboard": "Kopēt PNG starpliktuvē",
|
||||
"scale": "Mērogs",
|
||||
"save": "Saglabāt pašreizējo datni",
|
||||
"saveAs": "Saglabāt kā",
|
||||
"load": "Atvērt",
|
||||
|
@ -363,6 +355,30 @@
|
|||
"resetLibrary": "Atiestatīt bibliotēku",
|
||||
"removeItemsFromLib": "Noņemt atlasītos vienumus no bibliotēkas"
|
||||
},
|
||||
"imageExportDialog": {
|
||||
"header": "",
|
||||
"label": {
|
||||
"withBackground": "",
|
||||
"onlySelected": "",
|
||||
"darkMode": "",
|
||||
"embedScene": "",
|
||||
"scale": "",
|
||||
"padding": ""
|
||||
},
|
||||
"tooltip": {
|
||||
"embedScene": ""
|
||||
},
|
||||
"title": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
},
|
||||
"button": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
}
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "Jūsu zīmējumi ir šifrēti no gala līdz galam; līdz ar to Excalidraw serveri tos nekad neredzēs.",
|
||||
"link": "Ieraksts par šifrēšanu no gala līdz galam Excalidraw blogā"
|
||||
|
|
|
@ -40,10 +40,6 @@
|
|||
"arrowhead_triangle": "त्रिकोण",
|
||||
"fontSize": "अक्षर आकार",
|
||||
"fontFamily": "अक्षर समूह",
|
||||
"onlySelected": "नुसता निवडलेला",
|
||||
"withBackground": "पार्श्वभूमी",
|
||||
"exportEmbedScene": "दृश्य रुतवून टाका",
|
||||
"exportEmbedScene_details": "दृश्य डेटा पण एक्सपोर्ट केलेल्या पी-एन-जी/एस-वी-जी फाईलमध्ये जतन केला जाईल जेणेकरून त्यामधून दृश्य परत आणता येईल.\nनिर्यात केलेल्या फाइलचा आकार वाढेल.",
|
||||
"addWatermark": "\"एक्सकेलीड्रॉ ने बनवलेलं\" जोडा",
|
||||
"handDrawn": "हातानी बनवलेलं",
|
||||
"normal": "सामान्य",
|
||||
|
@ -100,7 +96,6 @@
|
|||
"flipHorizontal": "आडवं फ्लिप करा",
|
||||
"flipVertical": "उभं फ्लिप करा",
|
||||
"viewMode": "पहायची पद्धत",
|
||||
"toggleExportColorScheme": "रंग शैली जतन पलटा",
|
||||
"share": "सामायिक करा",
|
||||
"showStroke": "रेघ रंग निवड यंत्र दाखवा",
|
||||
"showBackground": "पार्श्वभूमि: रंग निवड यंत्र दाखवा",
|
||||
|
@ -128,7 +123,8 @@
|
|||
"unlockAll": "सर्व कुलूपातून बाहेर"
|
||||
},
|
||||
"statusPublished": "प्रकाशित करा",
|
||||
"sidebarLock": "साइडबार उघडं ठेवा"
|
||||
"sidebarLock": "साइडबार उघडं ठेवा",
|
||||
"eyeDropper": ""
|
||||
},
|
||||
"library": {
|
||||
"noItems": "अजून कोणतेही आइटम जोडलेले नाही...",
|
||||
|
@ -140,11 +136,7 @@
|
|||
"exportJSON": "फ़ाइलमधे बाहेर ठेवा",
|
||||
"exportImage": "प्रतिमा निर्यात करा...",
|
||||
"export": "येथे सुरक्षित करा...",
|
||||
"exportToPng": "पी-एन-जी स्वरूपात बाहेर ठेवा",
|
||||
"exportToSvg": "एस-वी-जी स्वरूपात बाहेर ठेवा",
|
||||
"copyToClipboard": "फळी वर कॉपी करा",
|
||||
"copyPngToClipboard": "PNG रूपे फळी वर कॉपी करा",
|
||||
"scale": "मोजपट्टी",
|
||||
"save": "वर्तमान फ़ाइल मधे जतन करा",
|
||||
"saveAs": "ह्या नावाने जतन करा",
|
||||
"load": "उघडा",
|
||||
|
@ -208,10 +200,10 @@
|
|||
"collabSaveFailed": "काही कारणा निमित्त आतल्या डेटाबेसमध्ये जतन करू शकत नाही। समस्या तशिस राहिल्यास, तुम्ही तुमचे काम गमावणार नाही याची खात्री करण्यासाठी तुम्ही तुमची फाइल स्थानिक जतन करावी.",
|
||||
"collabSaveFailed_sizeExceeded": "लगता है कि पृष्ठ तल काफ़ी बड़ा है, इस्कारण अंदरूनी डेटाबेस में सहेजा नहीं जा सका। किये काम को खोने न देने के लिये अपनी फ़ाइल को स्थानीय रूप से सहेजे।\n\nबॅकएंड डेटाबेसमध्ये जतन करू शकत नाही, कॅनव्हास खूप मोठा असल्याचे दिसते. तुम्ही तुमचे काम गमावणार नाही याची खात्री करण्यासाठी तुम्ही फाइल स्थानिक पातळीवर जतन करावी.",
|
||||
"brave_measure_text_error": {
|
||||
"line1": "",
|
||||
"line2": "",
|
||||
"line3": "",
|
||||
"line4": ""
|
||||
"line1": "असं वाटते की तुम्हीं Brave ब्राउज़र वापरतात आहात आणि त्या बरोबार <bold>आक्रामक पद्धति चें बोटांचे ठसे </bold> हां सेटिंग्स चा विकल्प चयन केलेला आहे.",
|
||||
"line2": "हे तुमच्या चित्रांच्या <bold>पाठ तत्वांनां </bold> खंडित करू शकतात.",
|
||||
"line3": "तुम्हाला आमच्या कड़ून खूप आग्रह आहे की हे सेटिंग्स मधले चयन नका करु. <link>हे अनुक्रम </link> हे कसे करावे हे दाखवु शकते.",
|
||||
"line4": "ही सेटिंग अक्षम करूनही पृष्ठ योग्यरित्या प्रदर्शित होत नसल्यास, आमच्या GitHub वर <issueLink>समस्या</issueLink> सबमिट करा, किव्हा <discordLink>डिस्कॉर्ड</discordLink> वर आम्हाला लिहा"
|
||||
}
|
||||
},
|
||||
"toolBar": {
|
||||
|
@ -363,6 +355,30 @@
|
|||
"resetLibrary": "संग्रह पुनर्स्थित करा",
|
||||
"removeItemsFromLib": "निवडलेले आयटम्स संग्रहातून काढा"
|
||||
},
|
||||
"imageExportDialog": {
|
||||
"header": "",
|
||||
"label": {
|
||||
"withBackground": "",
|
||||
"onlySelected": "",
|
||||
"darkMode": "",
|
||||
"embedScene": "",
|
||||
"scale": "",
|
||||
"padding": ""
|
||||
},
|
||||
"tooltip": {
|
||||
"embedScene": ""
|
||||
},
|
||||
"title": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
},
|
||||
"button": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
}
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "तुमचे चित्रे या टोकापासून त्या टोकापर्यंत कूटबद्धतित आहेत, त्या कारणांनी ऐक्सकैलिड्रॉ सर्वर्स ह्यानां ते पहाणं कधीच जमणार नाही.",
|
||||
"link": "ब्लॉग पोस्ट ऐक्सकैलिड्रॉ मधे या टोकापासून त्या टोकापर्यंत कूटबद्धतित आहेत"
|
||||
|
@ -395,20 +411,20 @@
|
|||
},
|
||||
"colors": {
|
||||
"transparent": "पारदर्शक",
|
||||
"black": "",
|
||||
"white": "",
|
||||
"red": "",
|
||||
"pink": "",
|
||||
"grape": "",
|
||||
"violet": "",
|
||||
"gray": "",
|
||||
"blue": "",
|
||||
"cyan": "",
|
||||
"teal": "",
|
||||
"green": "",
|
||||
"yellow": "",
|
||||
"orange": "",
|
||||
"bronze": ""
|
||||
"black": "काळा",
|
||||
"white": "पांढरा",
|
||||
"red": "लाल",
|
||||
"pink": "गुलाबी",
|
||||
"grape": "अंगूरी",
|
||||
"violet": "जांभळा",
|
||||
"gray": "राखाडी",
|
||||
"blue": "निळा",
|
||||
"cyan": "आकाशी",
|
||||
"teal": "हिरवट निळा",
|
||||
"green": "हिरवा",
|
||||
"yellow": "पिवळा",
|
||||
"orange": "केशरी",
|
||||
"bronze": "कांस्य"
|
||||
},
|
||||
"welcomeScreen": {
|
||||
"app": {
|
||||
|
@ -424,10 +440,10 @@
|
|||
}
|
||||
},
|
||||
"colorPicker": {
|
||||
"mostUsedCustomColors": "",
|
||||
"colors": "",
|
||||
"shades": "",
|
||||
"hexCode": "",
|
||||
"noShades": ""
|
||||
"mostUsedCustomColors": "सर्वात जास्त वापरणीस रंग",
|
||||
"colors": "रंग",
|
||||
"shades": "रंगछटा",
|
||||
"hexCode": "हेक्स कोड",
|
||||
"noShades": "ह्या रंगाच्या छटा उपलब्ध नाहित"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,10 +40,6 @@
|
|||
"arrowhead_triangle": "",
|
||||
"fontSize": "စာလုံးအရွယ်",
|
||||
"fontFamily": "စာလုံးပုံစံ",
|
||||
"onlySelected": "ရွေးထားသလောက်",
|
||||
"withBackground": "",
|
||||
"exportEmbedScene": "",
|
||||
"exportEmbedScene_details": "ထုတ်ယူလိုက်သော PNG/SVG ထဲမြင်ကွင်းအချက်အလက်များပါဝင်သဖြင့် ပြန်လည်ရယူနိုင်သော်လည်း ဖိုင်အရွယ်အစားကြီးပါမည်။",
|
||||
"addWatermark": "\"Excalidraw ဖြင့်ဖန်တီးသည်။\" စာသားထည့်",
|
||||
"handDrawn": "လက်ရေး",
|
||||
"normal": "ပုံမှန်",
|
||||
|
@ -100,7 +96,6 @@
|
|||
"flipHorizontal": "",
|
||||
"flipVertical": "",
|
||||
"viewMode": "",
|
||||
"toggleExportColorScheme": "",
|
||||
"share": "",
|
||||
"showStroke": "",
|
||||
"showBackground": "",
|
||||
|
@ -128,7 +123,8 @@
|
|||
"unlockAll": ""
|
||||
},
|
||||
"statusPublished": "",
|
||||
"sidebarLock": ""
|
||||
"sidebarLock": "",
|
||||
"eyeDropper": ""
|
||||
},
|
||||
"library": {
|
||||
"noItems": "",
|
||||
|
@ -140,11 +136,7 @@
|
|||
"exportJSON": "",
|
||||
"exportImage": "",
|
||||
"export": "",
|
||||
"exportToPng": "PNG ထုတ်",
|
||||
"exportToSvg": "SVG ထုတ်",
|
||||
"copyToClipboard": "ကူးယူ",
|
||||
"copyPngToClipboard": "PNG ကူးယူ",
|
||||
"scale": "စကေး",
|
||||
"save": "",
|
||||
"saveAs": "ပြောင်းသိမ်း",
|
||||
"load": "",
|
||||
|
@ -363,6 +355,30 @@
|
|||
"resetLibrary": "",
|
||||
"removeItemsFromLib": ""
|
||||
},
|
||||
"imageExportDialog": {
|
||||
"header": "",
|
||||
"label": {
|
||||
"withBackground": "",
|
||||
"onlySelected": "",
|
||||
"darkMode": "",
|
||||
"embedScene": "",
|
||||
"scale": "",
|
||||
"padding": ""
|
||||
},
|
||||
"tooltip": {
|
||||
"embedScene": ""
|
||||
},
|
||||
"title": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
},
|
||||
"button": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
}
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "ရေးဆွဲထားသောပုံများအား နှစ်ဘက်စွန်းတိုင်လျှို့ဝှက်ထားသဖြင့် Excalidraw ၏ဆာဗာများပင်လျှင်မြင်တွေ့ရမည်မဟုတ်ပါ။",
|
||||
"link": ""
|
||||
|
|
|
@ -40,10 +40,6 @@
|
|||
"arrowhead_triangle": "Trekant",
|
||||
"fontSize": "Skriftstørrelse",
|
||||
"fontFamily": "Fontfamilie",
|
||||
"onlySelected": "Kun valgte",
|
||||
"withBackground": "Bakgrunn",
|
||||
"exportEmbedScene": "Bygg inn scene",
|
||||
"exportEmbedScene_details": "Scenedata vil bli lagret i den eksporterte PNG/SVG-filen, slik at scenen kan gjenopprettes fra den.\nDet vil øke den eksporterte filstørrelsen.",
|
||||
"addWatermark": "Legg til \"Laget med Excalidraw\"",
|
||||
"handDrawn": "Håndtegnet",
|
||||
"normal": "Normal",
|
||||
|
@ -100,7 +96,6 @@
|
|||
"flipHorizontal": "Snu horisontalt",
|
||||
"flipVertical": "Snu vertikalt",
|
||||
"viewMode": "Visningsmodus",
|
||||
"toggleExportColorScheme": "Veksle eksport av fargepalett",
|
||||
"share": "Del",
|
||||
"showStroke": "Vis fargevelger for kantfarge",
|
||||
"showBackground": "Vis fargevelger for bakgrunnsfarge",
|
||||
|
@ -128,7 +123,8 @@
|
|||
"unlockAll": "Lås opp alle"
|
||||
},
|
||||
"statusPublished": "Publisert",
|
||||
"sidebarLock": "Holde sidemenyen åpen"
|
||||
"sidebarLock": "Holde sidemenyen åpen",
|
||||
"eyeDropper": ""
|
||||
},
|
||||
"library": {
|
||||
"noItems": "Ingen elementer lagt til ennå...",
|
||||
|
@ -140,11 +136,7 @@
|
|||
"exportJSON": "Eksporter til fil",
|
||||
"exportImage": "Eksporter bilde...",
|
||||
"export": "Lagre som...",
|
||||
"exportToPng": "Eksporter til PNG",
|
||||
"exportToSvg": "Eksporter til SVG",
|
||||
"copyToClipboard": "Kopier til utklippstavle",
|
||||
"copyPngToClipboard": "Kopier PNG til utklippstavlen",
|
||||
"scale": "Skalering",
|
||||
"save": "Lagre til aktiv fil",
|
||||
"saveAs": "Lagre som",
|
||||
"load": "Åpne",
|
||||
|
@ -363,6 +355,30 @@
|
|||
"resetLibrary": "Nullstill bibliotek",
|
||||
"removeItemsFromLib": "Fjern valgte elementer fra bibliotek"
|
||||
},
|
||||
"imageExportDialog": {
|
||||
"header": "Eksporter bilde",
|
||||
"label": {
|
||||
"withBackground": "Bakgrunn",
|
||||
"onlySelected": "Kun valgte",
|
||||
"darkMode": "Mørk modus",
|
||||
"embedScene": "Bygg inn scene",
|
||||
"scale": "Skalering",
|
||||
"padding": "Avstand"
|
||||
},
|
||||
"tooltip": {
|
||||
"embedScene": "Scenedata vil bli lagret i den eksporterte PNG/SVG-filen, slik at scenen kan gjenopprettes fra den.\nDet vil øke den eksporterte filstørrelsen."
|
||||
},
|
||||
"title": {
|
||||
"exportToPng": "Eksporter til PNG",
|
||||
"exportToSvg": "Eksporter til SVG",
|
||||
"copyPngToClipboard": "Kopier PNG til utklippstavlen"
|
||||
},
|
||||
"button": {
|
||||
"exportToPng": "PNG",
|
||||
"exportToSvg": "SVG",
|
||||
"copyPngToClipboard": "Kopier til utklippstavle"
|
||||
}
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "Dine tegninger er ende-til-ende-krypterte slik at Excalidraw sine servere aldri vil se dem.",
|
||||
"link": "Blogginnlegg om ende-til-ende-kryptering i Excalidraw"
|
||||
|
@ -395,20 +411,20 @@
|
|||
},
|
||||
"colors": {
|
||||
"transparent": "Gjennomsiktig",
|
||||
"black": "",
|
||||
"white": "",
|
||||
"red": "",
|
||||
"pink": "",
|
||||
"grape": "",
|
||||
"violet": "",
|
||||
"gray": "",
|
||||
"blue": "",
|
||||
"cyan": "",
|
||||
"teal": "",
|
||||
"green": "",
|
||||
"yellow": "",
|
||||
"orange": "",
|
||||
"bronze": ""
|
||||
"black": "Svart",
|
||||
"white": "Hvit",
|
||||
"red": "Rød",
|
||||
"pink": "Rosa",
|
||||
"grape": "Drue",
|
||||
"violet": "Fiolett",
|
||||
"gray": "Grå",
|
||||
"blue": "Blå",
|
||||
"cyan": "Turkis",
|
||||
"teal": "Blågrønn",
|
||||
"green": "Grønn",
|
||||
"yellow": "Gul",
|
||||
"orange": "Oransje",
|
||||
"bronze": "Bronse"
|
||||
},
|
||||
"welcomeScreen": {
|
||||
"app": {
|
||||
|
@ -424,10 +440,10 @@
|
|||
}
|
||||
},
|
||||
"colorPicker": {
|
||||
"mostUsedCustomColors": "",
|
||||
"colors": "",
|
||||
"shades": "",
|
||||
"hexCode": "",
|
||||
"noShades": ""
|
||||
"mostUsedCustomColors": "Mest brukte egendefinerte farger",
|
||||
"colors": "Farger",
|
||||
"shades": "Toner",
|
||||
"hexCode": "Heksadesimal kode",
|
||||
"noShades": "Ingen toner tilgjengelig for denne fargen"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,10 +40,6 @@
|
|||
"arrowhead_triangle": "Driehoek",
|
||||
"fontSize": "Tekstgrootte",
|
||||
"fontFamily": "Lettertype",
|
||||
"onlySelected": "Enkel geselecteerde",
|
||||
"withBackground": "Achtergrond",
|
||||
"exportEmbedScene": "Scène insluiten",
|
||||
"exportEmbedScene_details": "Scènegegevens worden in het geëxporteerde PNG/SVG-bestand opgeslagen zodat de scène kan worden hersteld.\nDe grootte van de geëxporteerde bestanden zal toenemen.",
|
||||
"addWatermark": "Voeg \"Gemaakt met Excalidraw\" toe",
|
||||
"handDrawn": "Handgetekend",
|
||||
"normal": "Normaal",
|
||||
|
@ -100,7 +96,6 @@
|
|||
"flipHorizontal": "Horizontaal spiegelen",
|
||||
"flipVertical": "Verticaal spiegelen",
|
||||
"viewMode": "Weergavemodus",
|
||||
"toggleExportColorScheme": "Kleurenschema exporteren aan/uit",
|
||||
"share": "Deel",
|
||||
"showStroke": "Toon lijn kleur kiezer",
|
||||
"showBackground": "Toon achtergrondkleur kiezer",
|
||||
|
@ -128,7 +123,8 @@
|
|||
"unlockAll": "Ontgrendel alles"
|
||||
},
|
||||
"statusPublished": "Gepubliceerd",
|
||||
"sidebarLock": "Zijbalk open houden"
|
||||
"sidebarLock": "Zijbalk open houden",
|
||||
"eyeDropper": ""
|
||||
},
|
||||
"library": {
|
||||
"noItems": "Nog geen items toegevoegd...",
|
||||
|
@ -140,11 +136,7 @@
|
|||
"exportJSON": "Exporteren naar bestand",
|
||||
"exportImage": "Exporteer afbeelding...",
|
||||
"export": "Sla op...",
|
||||
"exportToPng": "Exporteren naar PNG",
|
||||
"exportToSvg": "Exporteren naar SVG",
|
||||
"copyToClipboard": "Kopieer",
|
||||
"copyPngToClipboard": "Kopieer als PNG",
|
||||
"scale": "Schaal",
|
||||
"save": "Opslaan naar huidige bestand",
|
||||
"saveAs": "Opslaan als",
|
||||
"load": "Open",
|
||||
|
@ -363,6 +355,30 @@
|
|||
"resetLibrary": "Reset bibliotheek",
|
||||
"removeItemsFromLib": "Verwijder geselecteerde items uit bibliotheek"
|
||||
},
|
||||
"imageExportDialog": {
|
||||
"header": "",
|
||||
"label": {
|
||||
"withBackground": "",
|
||||
"onlySelected": "",
|
||||
"darkMode": "",
|
||||
"embedScene": "",
|
||||
"scale": "",
|
||||
"padding": ""
|
||||
},
|
||||
"tooltip": {
|
||||
"embedScene": ""
|
||||
},
|
||||
"title": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
},
|
||||
"button": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
}
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "Je tekeningen zijn beveiligd met end-to-end encryptie, dus Excalidraw's servers zullen nooit zien wat je tekent.",
|
||||
"link": "Blog post over end-to-end versleuteling in Excalidraw"
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue