Merge branch 'master' into zsviczian-loop-lock

# Conflicts:
#	packages/excalidraw/actions/actionProperties.tsx
#	packages/excalidraw/components/RadioSelection.tsx
This commit is contained in:
dwelle 2025-05-15 13:23:41 +02:00
commit d645e0ed13
23 changed files with 481 additions and 449 deletions

View File

@ -52,7 +52,7 @@
transform: none; transform: none;
} }
.excalidraw .panelColumn { .excalidraw .selected-shape-actions {
text-align: left; text-align: left;
} }

View File

@ -13,7 +13,7 @@
"default": "./dist/prod/index.js" "default": "./dist/prod/index.js"
}, },
"./*": { "./*": {
"types": "./../common/dist/types/common/src/*.d.ts" "types": "./dist/types/common/src/*.d.ts"
} }
}, },
"files": [ "files": [

View File

@ -10,6 +10,7 @@ export const isDarwin = /Mac|iPod|iPhone|iPad/.test(navigator.platform);
export const isWindows = /^Win/.test(navigator.platform); export const isWindows = /^Win/.test(navigator.platform);
export const isAndroid = /\b(android)\b/i.test(navigator.userAgent); export const isAndroid = /\b(android)\b/i.test(navigator.userAgent);
export const isFirefox = export const isFirefox =
typeof window !== "undefined" &&
"netscape" in window && "netscape" in window &&
navigator.userAgent.indexOf("rv:") > 1 && navigator.userAgent.indexOf("rv:") > 1 &&
navigator.userAgent.indexOf("Gecko") > 1; navigator.userAgent.indexOf("Gecko") > 1;
@ -255,7 +256,7 @@ export const EXPORT_DATA_TYPES = {
excalidrawClipboardWithAPI: "excalidraw-api/clipboard", excalidrawClipboardWithAPI: "excalidraw-api/clipboard",
} as const; } as const;
export const EXPORT_SOURCE = export const getExportSource = () =>
window.EXCALIDRAW_EXPORT_SOURCE || window.location.origin; window.EXCALIDRAW_EXPORT_SOURCE || window.location.origin;
// time in milliseconds // time in milliseconds

View File

@ -46,7 +46,7 @@ export const FONT_METADATA: Record<number, FontMetadata> = {
unitsPerEm: 1000, unitsPerEm: 1000,
ascender: 1011, ascender: 1011,
descender: -353, descender: -353,
lineHeight: 1.35, lineHeight: 1.25,
}, },
}, },
[FONT_FAMILY["Lilita One"]]: { [FONT_FAMILY["Lilita One"]]: {
@ -116,7 +116,7 @@ export const FONT_METADATA: Record<number, FontMetadata> = {
unitsPerEm: 1000, unitsPerEm: 1000,
ascender: 880, ascender: 880,
descender: -144, descender: -144,
lineHeight: 1.15, lineHeight: 1.25,
}, },
fallback: true, fallback: true,
}, },

View File

@ -13,7 +13,7 @@
"default": "./dist/prod/index.js" "default": "./dist/prod/index.js"
}, },
"./*": { "./*": {
"types": "./../element/dist/types/element/src/*.d.ts" "types": "./dist/types/element/src/*.d.ts"
} }
}, },
"files": [ "files": [

View File

@ -33,6 +33,8 @@ const RE_GH_GIST = /^https:\/\/gist\.github\.com\/([\w_-]+)\/([\w_-]+)/;
const RE_GH_GIST_EMBED = const RE_GH_GIST_EMBED =
/^<script[\s\S]*?\ssrc=["'](https:\/\/gist\.github\.com\/.*?)\.js["']/i; /^<script[\s\S]*?\ssrc=["'](https:\/\/gist\.github\.com\/.*?)\.js["']/i;
const RE_MSFORMS = /^(?:https?:\/\/)?forms\.microsoft\.com\//;
// not anchored to start to allow <blockquote> twitter embeds // not anchored to start to allow <blockquote> twitter embeds
const RE_TWITTER = const RE_TWITTER =
/(?:https?:\/\/)?(?:(?:w){3}\.)?(?:twitter|x)\.com\/[^/]+\/status\/(\d+)/; /(?:https?:\/\/)?(?:(?:w){3}\.)?(?:twitter|x)\.com\/[^/]+\/status\/(\d+)/;
@ -69,6 +71,7 @@ const ALLOWED_DOMAINS = new Set([
"val.town", "val.town",
"giphy.com", "giphy.com",
"reddit.com", "reddit.com",
"forms.microsoft.com",
]); ]);
const ALLOW_SAME_ORIGIN = new Set([ const ALLOW_SAME_ORIGIN = new Set([
@ -82,6 +85,7 @@ const ALLOW_SAME_ORIGIN = new Set([
"*.simplepdf.eu", "*.simplepdf.eu",
"stackblitz.com", "stackblitz.com",
"reddit.com", "reddit.com",
"forms.microsoft.com",
]); ]);
export const createSrcDoc = (body: string) => { export const createSrcDoc = (body: string) => {
@ -206,6 +210,10 @@ export const getEmbedLink = (
}; };
} }
if (RE_MSFORMS.test(link) && !link.includes("embed=true")) {
link += link.includes("?") ? "&embed=true" : "?embed=true";
}
if (RE_TWITTER.test(link)) { if (RE_TWITTER.test(link)) {
const postId = link.match(RE_TWITTER)![1]; const postId = link.match(RE_TWITTER)![1];
// the embed srcdoc still supports twitter.com domain only. // the embed srcdoc still supports twitter.com domain only.

View File

@ -351,12 +351,20 @@ const generateElementCanvas = (
export const DEFAULT_LINK_SIZE = 14; export const DEFAULT_LINK_SIZE = 14;
const IMAGE_PLACEHOLDER_IMG = document.createElement("img"); const IMAGE_PLACEHOLDER_IMG =
typeof document !== "undefined"
? document.createElement("img")
: ({ src: "" } as HTMLImageElement); // mock image element outside of browser
IMAGE_PLACEHOLDER_IMG.src = `data:${MIME_TYPES.svg},${encodeURIComponent( IMAGE_PLACEHOLDER_IMG.src = `data:${MIME_TYPES.svg},${encodeURIComponent(
`<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="image" class="svg-inline--fa fa-image fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="#888" d="M464 448H48c-26.51 0-48-21.49-48-48V112c0-26.51 21.49-48 48-48h416c26.51 0 48 21.49 48 48v288c0 26.51-21.49 48-48 48zM112 120c-30.928 0-56 25.072-56 56s25.072 56 56 56 56-25.072 56-56-25.072-56-56-56zM64 384h384V272l-87.515-87.515c-4.686-4.686-12.284-4.686-16.971 0L208 320l-55.515-55.515c-4.686-4.686-12.284-4.686-16.971 0L64 336v48z"></path></svg>`, `<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="image" class="svg-inline--fa fa-image fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="#888" d="M464 448H48c-26.51 0-48-21.49-48-48V112c0-26.51 21.49-48 48-48h416c26.51 0 48 21.49 48 48v288c0 26.51-21.49 48-48 48zM112 120c-30.928 0-56 25.072-56 56s25.072 56 56 56 56-25.072 56-56-25.072-56-56-56zM64 384h384V272l-87.515-87.515c-4.686-4.686-12.284-4.686-16.971 0L208 320l-55.515-55.515c-4.686-4.686-12.284-4.686-16.971 0L64 336v48z"></path></svg>`,
)}`; )}`;
const IMAGE_ERROR_PLACEHOLDER_IMG = document.createElement("img"); const IMAGE_ERROR_PLACEHOLDER_IMG =
typeof document !== "undefined"
? document.createElement("img")
: ({ src: "" } as HTMLImageElement); // mock image element outside of browser
IMAGE_ERROR_PLACEHOLDER_IMG.src = `data:${MIME_TYPES.svg},${encodeURIComponent( IMAGE_ERROR_PLACEHOLDER_IMG.src = `data:${MIME_TYPES.svg},${encodeURIComponent(
`<svg viewBox="0 0 668 668" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2"><path d="M464 448H48c-26.51 0-48-21.49-48-48V112c0-26.51 21.49-48 48-48h416c26.51 0 48 21.49 48 48v288c0 26.51-21.49 48-48 48ZM112 120c-30.928 0-56 25.072-56 56s25.072 56 56 56 56-25.072 56-56-25.072-56-56-56ZM64 384h384V272l-87.515-87.515c-4.686-4.686-12.284-4.686-16.971 0L208 320l-55.515-55.515c-4.686-4.686-12.284-4.686-16.971 0L64 336v48Z" style="fill:#888;fill-rule:nonzero" transform="matrix(.81709 0 0 .81709 124.825 145.825)"/><path d="M256 8C119.034 8 8 119.033 8 256c0 136.967 111.034 248 248 248s248-111.034 248-248S392.967 8 256 8Zm130.108 117.892c65.448 65.448 70 165.481 20.677 235.637L150.47 105.216c70.204-49.356 170.226-44.735 235.638 20.676ZM125.892 386.108c-65.448-65.448-70-165.481-20.677-235.637L361.53 406.784c-70.203 49.356-170.226 44.736-235.638-20.676Z" style="fill:#888;fill-rule:nonzero" transform="matrix(.30366 0 0 .30366 506.822 60.065)"/></svg>`, `<svg viewBox="0 0 668 668" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2"><path d="M464 448H48c-26.51 0-48-21.49-48-48V112c0-26.51 21.49-48 48-48h416c26.51 0 48 21.49 48 48v288c0 26.51-21.49 48-48 48ZM112 120c-30.928 0-56 25.072-56 56s25.072 56 56 56 56-25.072 56-56-25.072-56-56-56ZM64 384h384V272l-87.515-87.515c-4.686-4.686-12.284-4.686-16.971 0L208 320l-55.515-55.515c-4.686-4.686-12.284-4.686-16.971 0L64 336v48Z" style="fill:#888;fill-rule:nonzero" transform="matrix(.81709 0 0 .81709 124.825 145.825)"/><path d="M256 8C119.034 8 8 119.033 8 256c0 136.967 111.034 248 248 248s248-111.034 248-248S392.967 8 256 8Zm130.108 117.892c65.448 65.448 70 165.481 20.677 235.637L150.47 105.216c70.204-49.356 170.226-44.735 235.638 20.676ZM125.892 386.108c-65.448-65.448-70-165.481-20.677-235.637L361.53 406.784c-70.203 49.356-170.226 44.736-235.638-20.676Z" style="fill:#888;fill-rule:nonzero" transform="matrix(.30366 0 0 .30366 506.822 60.065)"/></svg>`,
)}`; )}`;

View File

@ -78,7 +78,7 @@ import type { Scene } from "@excalidraw/element";
import type { CaptureUpdateActionType } from "@excalidraw/element"; import type { CaptureUpdateActionType } from "@excalidraw/element";
import { trackEvent } from "../analytics"; import { trackEvent } from "../analytics";
import { ButtonIconSelect } from "../components/ButtonIconSelect"; import { RadioSelection } from "../components/RadioSelection";
import { ColorPicker } from "../components/ColorPicker/ColorPicker"; import { ColorPicker } from "../components/ColorPicker/ColorPicker";
import { FontPicker } from "../components/FontPicker/FontPicker"; import { FontPicker } from "../components/FontPicker/FontPicker";
import { IconPicker } from "../components/IconPicker"; import { IconPicker } from "../components/IconPicker";
@ -457,50 +457,52 @@ export const actionChangeFillStyle = register({
return ( return (
<fieldset> <fieldset>
<legend>{t("labels.fill")}</legend> <legend>{t("labels.fill")}</legend>
<ButtonIconSelect <div className="buttonList">
type="button" <RadioSelection
options={[ type="button"
{ options={[
value: "hachure", {
text: `${ value: "hachure",
allElementsZigZag ? t("labels.zigzag") : t("labels.hachure") text: `${
} (${getShortcutKey("Alt-Click")})`, allElementsZigZag ? t("labels.zigzag") : t("labels.hachure")
icon: allElementsZigZag ? FillZigZagIcon : FillHachureIcon, } (${getShortcutKey("Alt-Click")})`,
active: allElementsZigZag ? true : undefined, icon: allElementsZigZag ? FillZigZagIcon : FillHachureIcon,
testId: `fill-hachure`, active: allElementsZigZag ? true : undefined,
}, testId: `fill-hachure`,
{ },
value: "cross-hatch", {
text: t("labels.crossHatch"), value: "cross-hatch",
icon: FillCrossHatchIcon, text: t("labels.crossHatch"),
testId: `fill-cross-hatch`, icon: FillCrossHatchIcon,
}, testId: `fill-cross-hatch`,
{ },
value: "solid", {
text: t("labels.solid"), value: "solid",
icon: FillSolidIcon, text: t("labels.solid"),
testId: `fill-solid`, icon: FillSolidIcon,
}, testId: `fill-solid`,
]} },
value={getFormValue( ]}
elements, value={getFormValue(
app, elements,
(element) => element.fillStyle, app,
(element) => element.hasOwnProperty("fillStyle"), (element) => element.fillStyle,
(hasSelection) => (element) => element.hasOwnProperty("fillStyle"),
hasSelection ? null : appState.currentItemFillStyle, (hasSelection) =>
)} hasSelection ? null : appState.currentItemFillStyle,
onClick={(value, event) => { )}
const nextValue = onClick={(value, event) => {
event.altKey && const nextValue =
value === "hachure" && event.altKey &&
selectedElements.every((el) => el.fillStyle === "hachure") value === "hachure" &&
? "zigzag" selectedElements.every((el) => el.fillStyle === "hachure")
: value; ? "zigzag"
: value;
updateData(nextValue); updateData(nextValue);
}} }}
/> />
</div>
</fieldset> </fieldset>
); );
}, },
@ -524,38 +526,40 @@ export const actionChangeStrokeWidth = register({
PanelComponent: ({ elements, appState, updateData, app }) => ( PanelComponent: ({ elements, appState, updateData, app }) => (
<fieldset> <fieldset>
<legend>{t("labels.strokeWidth")}</legend> <legend>{t("labels.strokeWidth")}</legend>
<ButtonIconSelect <div className="buttonList">
group="stroke-width" <RadioSelection
options={[ group="stroke-width"
{ options={[
value: STROKE_WIDTH.thin, {
text: t("labels.thin"), value: STROKE_WIDTH.thin,
icon: StrokeWidthBaseIcon, text: t("labels.thin"),
testId: "strokeWidth-thin", icon: StrokeWidthBaseIcon,
}, testId: "strokeWidth-thin",
{ },
value: STROKE_WIDTH.bold, {
text: t("labels.bold"), value: STROKE_WIDTH.bold,
icon: StrokeWidthBoldIcon, text: t("labels.bold"),
testId: "strokeWidth-bold", icon: StrokeWidthBoldIcon,
}, testId: "strokeWidth-bold",
{ },
value: STROKE_WIDTH.extraBold, {
text: t("labels.extraBold"), value: STROKE_WIDTH.extraBold,
icon: StrokeWidthExtraBoldIcon, text: t("labels.extraBold"),
testId: "strokeWidth-extraBold", icon: StrokeWidthExtraBoldIcon,
}, testId: "strokeWidth-extraBold",
]} },
value={getFormValue( ]}
elements, value={getFormValue(
app, elements,
(element) => element.strokeWidth, app,
(element) => element.hasOwnProperty("strokeWidth"), (element) => element.strokeWidth,
(hasSelection) => (element) => element.hasOwnProperty("strokeWidth"),
hasSelection ? null : appState.currentItemStrokeWidth, (hasSelection) =>
)} hasSelection ? null : appState.currentItemStrokeWidth,
onChange={(value) => updateData(value)} )}
/> onChange={(value) => updateData(value)}
/>
</div>
</fieldset> </fieldset>
), ),
}); });
@ -579,35 +583,37 @@ export const actionChangeSloppiness = register({
PanelComponent: ({ elements, appState, updateData, app }) => ( PanelComponent: ({ elements, appState, updateData, app }) => (
<fieldset> <fieldset>
<legend>{t("labels.sloppiness")}</legend> <legend>{t("labels.sloppiness")}</legend>
<ButtonIconSelect <div className="buttonList">
group="sloppiness" <RadioSelection
options={[ group="sloppiness"
{ options={[
value: 0, {
text: t("labels.architect"), value: 0,
icon: SloppinessArchitectIcon, text: t("labels.architect"),
}, icon: SloppinessArchitectIcon,
{ },
value: 1, {
text: t("labels.artist"), value: 1,
icon: SloppinessArtistIcon, text: t("labels.artist"),
}, icon: SloppinessArtistIcon,
{ },
value: 2, {
text: t("labels.cartoonist"), value: 2,
icon: SloppinessCartoonistIcon, text: t("labels.cartoonist"),
}, icon: SloppinessCartoonistIcon,
]} },
value={getFormValue( ]}
elements, value={getFormValue(
app, elements,
(element) => element.roughness, app,
(element) => element.hasOwnProperty("roughness"), (element) => element.roughness,
(hasSelection) => (element) => element.hasOwnProperty("roughness"),
hasSelection ? null : appState.currentItemRoughness, (hasSelection) =>
)} hasSelection ? null : appState.currentItemRoughness,
onChange={(value) => updateData(value)} )}
/> onChange={(value) => updateData(value)}
/>
</div>
</fieldset> </fieldset>
), ),
}); });
@ -630,35 +636,37 @@ export const actionChangeStrokeStyle = register({
PanelComponent: ({ elements, appState, updateData, app }) => ( PanelComponent: ({ elements, appState, updateData, app }) => (
<fieldset> <fieldset>
<legend>{t("labels.strokeStyle")}</legend> <legend>{t("labels.strokeStyle")}</legend>
<ButtonIconSelect <div className="buttonList">
group="strokeStyle" <RadioSelection
options={[ group="strokeStyle"
{ options={[
value: "solid", {
text: t("labels.strokeStyle_solid"), value: "solid",
icon: StrokeWidthBaseIcon, text: t("labels.strokeStyle_solid"),
}, icon: StrokeWidthBaseIcon,
{ },
value: "dashed", {
text: t("labels.strokeStyle_dashed"), value: "dashed",
icon: StrokeStyleDashedIcon, text: t("labels.strokeStyle_dashed"),
}, icon: StrokeStyleDashedIcon,
{ },
value: "dotted", {
text: t("labels.strokeStyle_dotted"), value: "dotted",
icon: StrokeStyleDottedIcon, text: t("labels.strokeStyle_dotted"),
}, icon: StrokeStyleDottedIcon,
]} },
value={getFormValue( ]}
elements, value={getFormValue(
app, elements,
(element) => element.strokeStyle, app,
(element) => element.hasOwnProperty("strokeStyle"), (element) => element.strokeStyle,
(hasSelection) => (element) => element.hasOwnProperty("strokeStyle"),
hasSelection ? null : appState.currentItemStrokeStyle, (hasSelection) =>
)} hasSelection ? null : appState.currentItemStrokeStyle,
onChange={(value) => updateData(value)} )}
/> onChange={(value) => updateData(value)}
/>
</div>
</fieldset> </fieldset>
), ),
}); });
@ -697,63 +705,65 @@ export const actionChangeFontSize = register({
PanelComponent: ({ elements, appState, updateData, app }) => ( PanelComponent: ({ elements, appState, updateData, app }) => (
<fieldset> <fieldset>
<legend>{t("labels.fontSize")}</legend> <legend>{t("labels.fontSize")}</legend>
<ButtonIconSelect <div className="buttonList">
group="font-size" <RadioSelection
options={[ group="font-size"
{ options={[
value: 16, {
text: t("labels.small"), value: 16,
icon: FontSizeSmallIcon, text: t("labels.small"),
testId: "fontSize-small", icon: FontSizeSmallIcon,
}, testId: "fontSize-small",
{ },
value: 20, {
text: t("labels.medium"), value: 20,
icon: FontSizeMediumIcon, text: t("labels.medium"),
testId: "fontSize-medium", icon: FontSizeMediumIcon,
}, testId: "fontSize-medium",
{ },
value: 28, {
text: t("labels.large"), value: 28,
icon: FontSizeLargeIcon, text: t("labels.large"),
testId: "fontSize-large", icon: FontSizeLargeIcon,
}, testId: "fontSize-large",
{ },
value: 36, {
text: t("labels.veryLarge"), value: 36,
icon: FontSizeExtraLargeIcon, text: t("labels.veryLarge"),
testId: "fontSize-veryLarge", icon: FontSizeExtraLargeIcon,
}, testId: "fontSize-veryLarge",
]} },
value={getFormValue( ]}
elements, value={getFormValue(
app, elements,
(element) => { app,
if (isTextElement(element)) { (element) => {
return element.fontSize; if (isTextElement(element)) {
} return element.fontSize;
const boundTextElement = getBoundTextElement( }
element, const boundTextElement = getBoundTextElement(
app.scene.getNonDeletedElementsMap(), element,
); app.scene.getNonDeletedElementsMap(),
if (boundTextElement) { );
return boundTextElement.fontSize; if (boundTextElement) {
} return boundTextElement.fontSize;
return null; }
}, return null;
(element) => },
isTextElement(element) || (element) =>
getBoundTextElement( isTextElement(element) ||
element, getBoundTextElement(
app.scene.getNonDeletedElementsMap(), element,
) !== null, app.scene.getNonDeletedElementsMap(),
(hasSelection) => ) !== null,
hasSelection (hasSelection) =>
? null hasSelection
: appState.currentItemFontSize || DEFAULT_FONT_SIZE, ? null
)} : appState.currentItemFontSize || DEFAULT_FONT_SIZE,
onChange={(value) => updateData(value)} )}
/> onChange={(value) => updateData(value)}
/>
</div>
</fieldset> </fieldset>
), ),
}); });
@ -1225,52 +1235,54 @@ export const actionChangeTextAlign = register({
return ( return (
<fieldset> <fieldset>
<legend>{t("labels.textAlign")}</legend> <legend>{t("labels.textAlign")}</legend>
<ButtonIconSelect<TextAlign | false> <div className="buttonList">
group="text-align" <RadioSelection<TextAlign | false>
options={[ group="text-align"
{ options={[
value: "left", {
text: t("labels.left"), value: "left",
icon: TextAlignLeftIcon, text: t("labels.left"),
testId: "align-left", icon: TextAlignLeftIcon,
}, testId: "align-left",
{ },
value: "center", {
text: t("labels.center"), value: "center",
icon: TextAlignCenterIcon, text: t("labels.center"),
testId: "align-horizontal-center", icon: TextAlignCenterIcon,
}, testId: "align-horizontal-center",
{ },
value: "right", {
text: t("labels.right"), value: "right",
icon: TextAlignRightIcon, text: t("labels.right"),
testId: "align-right", icon: TextAlignRightIcon,
}, testId: "align-right",
]} },
value={getFormValue( ]}
elements, value={getFormValue(
app, elements,
(element) => { app,
if (isTextElement(element)) { (element) => {
return element.textAlign; if (isTextElement(element)) {
} return element.textAlign;
const boundTextElement = getBoundTextElement( }
element, const boundTextElement = getBoundTextElement(
elementsMap, element,
); elementsMap,
if (boundTextElement) { );
return boundTextElement.textAlign; if (boundTextElement) {
} return boundTextElement.textAlign;
return null; }
}, return null;
(element) => },
isTextElement(element) || (element) =>
getBoundTextElement(element, elementsMap) !== null, isTextElement(element) ||
(hasSelection) => getBoundTextElement(element, elementsMap) !== null,
hasSelection ? null : appState.currentItemTextAlign, (hasSelection) =>
)} hasSelection ? null : appState.currentItemTextAlign,
onChange={(value) => updateData(value)} )}
/> onChange={(value) => updateData(value)}
/>
</div>
</fieldset> </fieldset>
); );
}, },
@ -1313,54 +1325,56 @@ export const actionChangeVerticalAlign = register({
PanelComponent: ({ elements, appState, updateData, app }) => { PanelComponent: ({ elements, appState, updateData, app }) => {
return ( return (
<fieldset> <fieldset>
<ButtonIconSelect<VerticalAlign | false> <div className="buttonList">
group="text-align" <RadioSelection<VerticalAlign | false>
options={[ group="text-align"
{ options={[
value: VERTICAL_ALIGN.TOP, {
text: t("labels.alignTop"), value: VERTICAL_ALIGN.TOP,
icon: <TextAlignTopIcon theme={appState.theme} />, text: t("labels.alignTop"),
testId: "align-top", icon: <TextAlignTopIcon theme={appState.theme} />,
}, testId: "align-top",
{ },
value: VERTICAL_ALIGN.MIDDLE, {
text: t("labels.centerVertically"), value: VERTICAL_ALIGN.MIDDLE,
icon: <TextAlignMiddleIcon theme={appState.theme} />, text: t("labels.centerVertically"),
testId: "align-middle", icon: <TextAlignMiddleIcon theme={appState.theme} />,
}, testId: "align-middle",
{ },
value: VERTICAL_ALIGN.BOTTOM, {
text: t("labels.alignBottom"), value: VERTICAL_ALIGN.BOTTOM,
icon: <TextAlignBottomIcon theme={appState.theme} />, text: t("labels.alignBottom"),
testId: "align-bottom", icon: <TextAlignBottomIcon theme={appState.theme} />,
}, testId: "align-bottom",
]} },
value={getFormValue( ]}
elements, value={getFormValue(
app, elements,
(element) => { app,
if (isTextElement(element) && element.containerId) { (element) => {
return element.verticalAlign; if (isTextElement(element) && element.containerId) {
} return element.verticalAlign;
const boundTextElement = getBoundTextElement( }
element, const boundTextElement = getBoundTextElement(
app.scene.getNonDeletedElementsMap(), element,
); app.scene.getNonDeletedElementsMap(),
if (boundTextElement) { );
return boundTextElement.verticalAlign; if (boundTextElement) {
} return boundTextElement.verticalAlign;
return null; }
}, return null;
(element) => },
isTextElement(element) || (element) =>
getBoundTextElement( isTextElement(element) ||
element, getBoundTextElement(
app.scene.getNonDeletedElementsMap(), element,
) !== null, app.scene.getNonDeletedElementsMap(),
(hasSelection) => (hasSelection ? null : VERTICAL_ALIGN.MIDDLE), ) !== null,
)} (hasSelection) => (hasSelection ? null : VERTICAL_ALIGN.MIDDLE),
onChange={(value) => updateData(value)} )}
/> onChange={(value) => updateData(value)}
/>
</div>
</fieldset> </fieldset>
); );
}, },
@ -1408,34 +1422,39 @@ export const actionChangeRoundness = register({
return ( return (
<fieldset> <fieldset>
<legend>{t("labels.edges")}</legend> <legend>{t("labels.edges")}</legend>
<ButtonIconSelect <div className="buttonList">
group="edges" <RadioSelection
options={[ group="edges"
{ options={[
value: "sharp", {
text: t("labels.sharp"), value: "sharp",
icon: EdgeSharpIcon, text: t("labels.sharp"),
}, icon: EdgeSharpIcon,
{ },
value: "round", {
text: t("labels.round"), value: "round",
icon: EdgeRoundIcon, text: t("labels.round"),
}, icon: EdgeRoundIcon,
]} },
value={getFormValue( ]}
elements, value={getFormValue(
app, elements,
(element) => app,
hasLegacyRoundness ? null : element.roundness ? "round" : "sharp", (element) =>
(element) => hasLegacyRoundness
!isArrowElement(element) && element.hasOwnProperty("roundness"), ? null
(hasSelection) => : element.roundness
hasSelection ? null : appState.currentItemRoundness, ? "round"
)} : "sharp",
onChange={(value) => updateData(value)} (element) =>
> !isArrowElement(element) && element.hasOwnProperty("roundness"),
(hasSelection) =>
hasSelection ? null : appState.currentItemRoundness,
)}
onChange={(value) => updateData(value)}
/>
{renderAction("togglePolygon")} {renderAction("togglePolygon")}
</ButtonIconSelect> </div>
</fieldset> </fieldset>
); );
}, },
@ -1798,48 +1817,50 @@ export const actionChangeArrowType = register({
return ( return (
<fieldset> <fieldset>
<legend>{t("labels.arrowtypes")}</legend> <legend>{t("labels.arrowtypes")}</legend>
<ButtonIconSelect <div className="buttonList">
group="arrowtypes" <RadioSelection
options={[ group="arrowtypes"
{ options={[
value: ARROW_TYPE.sharp, {
text: t("labels.arrowtype_sharp"), value: ARROW_TYPE.sharp,
icon: sharpArrowIcon, text: t("labels.arrowtype_sharp"),
testId: "sharp-arrow", icon: sharpArrowIcon,
}, testId: "sharp-arrow",
{ },
value: ARROW_TYPE.round, {
text: t("labels.arrowtype_round"), value: ARROW_TYPE.round,
icon: roundArrowIcon, text: t("labels.arrowtype_round"),
testId: "round-arrow", icon: roundArrowIcon,
}, testId: "round-arrow",
{ },
value: ARROW_TYPE.elbow, {
text: t("labels.arrowtype_elbowed"), value: ARROW_TYPE.elbow,
icon: elbowArrowIcon, text: t("labels.arrowtype_elbowed"),
testId: "elbow-arrow", icon: elbowArrowIcon,
}, testId: "elbow-arrow",
]} },
value={getFormValue( ]}
elements, value={getFormValue(
app, elements,
(element) => { app,
if (isArrowElement(element)) { (element) => {
return element.elbowed if (isArrowElement(element)) {
? ARROW_TYPE.elbow return element.elbowed
: element.roundness ? ARROW_TYPE.elbow
? ARROW_TYPE.round : element.roundness
: ARROW_TYPE.sharp; ? ARROW_TYPE.round
} : ARROW_TYPE.sharp;
}
return null; return null;
}, },
(element) => isArrowElement(element), (element) => isArrowElement(element),
(hasSelection) => (hasSelection) =>
hasSelection ? null : appState.currentItemArrowType, hasSelection ? null : appState.currentItemArrowType,
)} )}
onChange={(value) => updateData(value)} onChange={(value) => updateData(value)}
/> />
</div>
</fieldset> </fieldset>
); );
}, },

View File

@ -154,7 +154,7 @@ export const SelectedShapeActions = ({
!isSingleElementBoundContainer && alignActionsPredicate(appState, app); !isSingleElementBoundContainer && alignActionsPredicate(appState, app);
return ( return (
<div className="panelColumn"> <div className="selected-shape-actions">
<div> <div>
{canChangeStrokeColor(appState, targetElements) && {canChangeStrokeColor(appState, targetElements) &&
renderAction("changeStrokeColor")} renderAction("changeStrokeColor")}

View File

@ -7276,8 +7276,13 @@ class App extends React.Component<AppProps, AppState> {
}); });
// If we click on something // If we click on something
} else if (hitElement != null) { } else if (hitElement != null) {
// == deep selection ==
// on CMD/CTRL, drill down to hit element regardless of groups etc. // on CMD/CTRL, drill down to hit element regardless of groups etc.
if (event[KEYS.CTRL_OR_CMD]) { if (event[KEYS.CTRL_OR_CMD]) {
if (event.altKey) {
// ctrl + alt means we're lasso selecting
return false;
}
if (!this.state.selectedElementIds[hitElement.id]) { if (!this.state.selectedElementIds[hitElement.id]) {
pointerDownState.hit.wasAddedToSelection = true; pointerDownState.hit.wasAddedToSelection = true;
} }
@ -8636,17 +8641,19 @@ class App extends React.Component<AppProps, AppState> {
pointerDownState.lastCoords.x = pointerCoords.x; pointerDownState.lastCoords.x = pointerCoords.x;
pointerDownState.lastCoords.y = pointerCoords.y; pointerDownState.lastCoords.y = pointerCoords.y;
if (event.altKey) { if (event.altKey) {
this.setActiveTool( flushSync(() => {
{ type: "lasso", fromSelection: true }, this.setActiveTool(
event.shiftKey, { type: "lasso", fromSelection: true },
); event.shiftKey,
this.lassoTrail.startPath( );
pointerDownState.origin.x, this.lassoTrail.startPath(
pointerDownState.origin.y, pointerDownState.origin.x,
event.shiftKey, pointerDownState.origin.y,
); event.shiftKey,
this.setAppState({ );
selectionElement: null, this.setAppState({
selectionElement: null,
});
}); });
} else { } else {
this.maybeDragNewGenericElement(pointerDownState, event); this.maybeDragNewGenericElement(pointerDownState, event);
@ -9521,7 +9528,10 @@ class App extends React.Component<AppProps, AppState> {
// if we're editing a line, pointerup shouldn't switch selection if // if we're editing a line, pointerup shouldn't switch selection if
// box selected // box selected
(!this.state.editingLinearElement || (!this.state.editingLinearElement ||
!pointerDownState.boxSelection.hasOccurred) !pointerDownState.boxSelection.hasOccurred) &&
// hitElement can be set when alt + ctrl to toggle lasso and we will
// just respect the selected elements from lasso instead
this.state.activeTool.type !== "lasso"
) { ) {
// when inside line editor, shift selects points instead // when inside line editor, shift selects points instead
if (childEvent.shiftKey && !this.state.editingLinearElement) { if (childEvent.shiftKey && !this.state.editingLinearElement) {

View File

@ -1,30 +0,0 @@
import clsx from "clsx";
export const ButtonSelect = <T extends Object>({
options,
value,
onChange,
group,
}: {
options: { value: T; text: string }[];
value: T | null;
onChange: (value: T) => void;
group: string;
}) => (
<div className="buttonList">
{options.map((option) => (
<label
key={option.text}
className={clsx({ active: value === option.value })}
>
<input
type="radio"
name={group}
onChange={() => onChange(option.value)}
checked={value === option.value}
/>
{option.text}
</label>
))}
</div>
);

View File

@ -6,7 +6,7 @@ import { FONT_FAMILY } from "@excalidraw/common";
import type { FontFamilyValues } from "@excalidraw/element/types"; import type { FontFamilyValues } from "@excalidraw/element/types";
import { t } from "../../i18n"; import { t } from "../../i18n";
import { ButtonIconSelect } from "../ButtonIconSelect"; import { RadioSelection } from "../RadioSelection";
import { ButtonSeparator } from "../ButtonSeparator"; import { ButtonSeparator } from "../ButtonSeparator";
import { import {
FontFamilyCodeIcon, FontFamilyCodeIcon,
@ -82,12 +82,14 @@ export const FontPicker = React.memo(
return ( return (
<div role="dialog" aria-modal="true" className="FontPicker__container"> <div role="dialog" aria-modal="true" className="FontPicker__container">
<ButtonIconSelect<FontFamilyValues | false> <div className="buttonList">
type="button" <RadioSelection<FontFamilyValues | false>
options={defaultFonts} type="button"
value={selectedFontFamily} options={defaultFonts}
onClick={onSelectCallback} value={selectedFontFamily}
/> onClick={onSelectCallback}
/>
</div>
<ButtonSeparator /> <ButtonSeparator />
<Popover.Root open={isOpened} onOpenChange={onPopupChange}> <Popover.Root open={isOpened} onOpenChange={onPopupChange}>
<FontPickerTrigger selectedFontFamily={selectedFontFamily} /> <FontPickerTrigger selectedFontFamily={selectedFontFamily} />

View File

@ -5,10 +5,10 @@ import { useCallback, useEffect, useRef, useState } from "react";
import { import {
EDITOR_LS_KEYS, EDITOR_LS_KEYS,
EXPORT_DATA_TYPES, EXPORT_DATA_TYPES,
EXPORT_SOURCE,
MIME_TYPES, MIME_TYPES,
VERSIONS, VERSIONS,
chunk, chunk,
getExportSource,
} from "@excalidraw/common"; } from "@excalidraw/common";
import { EditorLocalStorage } from "../data/EditorLocalStorage"; import { EditorLocalStorage } from "../data/EditorLocalStorage";
@ -281,7 +281,7 @@ const PublishLibrary = ({
const libContent: ExportedLibraryData = { const libContent: ExportedLibraryData = {
type: EXPORT_DATA_TYPES.excalidrawLibrary, type: EXPORT_DATA_TYPES.excalidrawLibrary,
version: VERSIONS.excalidrawLibrary, version: VERSIONS.excalidrawLibrary,
source: EXPORT_SOURCE, source: getExportSource(),
libraryItems: clonedLibItems, libraryItems: clonedLibItems,
}; };
const content = JSON.stringify(libContent, null, 2); const content = JSON.stringify(libContent, null, 2);

View File

@ -4,8 +4,7 @@ import { ButtonIcon } from "./ButtonIcon";
import type { JSX } from "react"; import type { JSX } from "react";
// TODO: It might be "clever" to add option.icon to the existing component <ButtonSelect /> export const RadioSelection = <T extends Object>(
export const ButtonIconSelect = <T extends Object>(
props: { props: {
options: { options: {
value: T; value: T;
@ -17,7 +16,6 @@ export const ButtonIconSelect = <T extends Object>(
}[]; }[];
value: T | null; value: T | null;
type?: "radio" | "button"; type?: "radio" | "button";
children?: React.ReactNode;
} & ( } & (
| { type?: "radio"; group: string; onChange: (value: T) => void } | { type?: "radio"; group: string; onChange: (value: T) => void }
| { | {
@ -29,7 +27,7 @@ export const ButtonIconSelect = <T extends Object>(
} }
), ),
) => ( ) => (
<div className="buttonList"> <>
{props.options.map((option) => {props.options.map((option) =>
props.type === "button" ? ( props.type === "button" ? (
<ButtonIcon <ButtonIcon
@ -57,6 +55,5 @@ export const ButtonIconSelect = <T extends Object>(
</label> </label>
), ),
)} )}
{props.children} </>
</div>
); );

View File

@ -4,8 +4,6 @@ import { MIME_TYPES } from "@excalidraw/common";
import { getElementAbsoluteCoords } from "@excalidraw/element"; import { getElementAbsoluteCoords } from "@excalidraw/element";
import { hitElementBoundingBox } from "@excalidraw/element"; import { hitElementBoundingBox } from "@excalidraw/element";
import { DEFAULT_LINK_SIZE } from "@excalidraw/element";
import type { GlobalPoint, Radians } from "@excalidraw/math"; import type { GlobalPoint, Radians } from "@excalidraw/math";
import type { Bounds } from "@excalidraw/element"; import type { Bounds } from "@excalidraw/element";
@ -16,9 +14,11 @@ import type {
import type { AppState, UIAppState } from "../../types"; import type { AppState, UIAppState } from "../../types";
export const DEFAULT_LINK_SIZE = 12;
export const EXTERNAL_LINK_IMG = document.createElement("img"); export const EXTERNAL_LINK_IMG = document.createElement("img");
EXTERNAL_LINK_IMG.src = `data:${MIME_TYPES.svg}, ${encodeURIComponent( EXTERNAL_LINK_IMG.src = `data:${MIME_TYPES.svg}, ${encodeURIComponent(
`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#1971c2" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-external-link"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg>`, `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#1971c2" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" class="feather feather-external-link"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg>`,
)}`; )}`;
export const ELEMENT_LINK_IMG = document.createElement("img"); export const ELEMENT_LINK_IMG = document.createElement("img");
@ -32,13 +32,14 @@ export const getLinkHandleFromCoords = (
appState: Pick<UIAppState, "zoom">, appState: Pick<UIAppState, "zoom">,
): Bounds => { ): Bounds => {
const size = DEFAULT_LINK_SIZE; const size = DEFAULT_LINK_SIZE;
const linkWidth = size / appState.zoom.value; const zoom = appState.zoom.value > 1 ? appState.zoom.value : 1;
const linkHeight = size / appState.zoom.value; const linkWidth = size / zoom;
const linkMarginY = size / appState.zoom.value; const linkHeight = size / zoom;
const linkMarginY = size / zoom;
const centerX = (x1 + x2) / 2; const centerX = (x1 + x2) / 2;
const centerY = (y1 + y2) / 2; const centerY = (y1 + y2) / 2;
const centeringOffset = (size - 8) / (2 * appState.zoom.value); const centeringOffset = (size - 8) / (2 * zoom);
const dashedLineMargin = 4 / appState.zoom.value; const dashedLineMargin = 4 / zoom;
// Same as `ne` resize handle // Same as `ne` resize handle
const x = x2 + dashedLineMargin - centeringOffset; const x = x2 + dashedLineMargin - centeringOffset;

View File

@ -140,7 +140,7 @@ body.excalidraw-cursor-resize * {
justify-content: space-between; justify-content: space-between;
} }
.panelColumn { .selected-shape-actions {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
row-gap: 0.75rem; row-gap: 0.75rem;
@ -245,10 +245,6 @@ body.excalidraw-cursor-resize * {
left: 0; left: 0;
right: 0; right: 0;
--bar-padding: calc(4 * var(--space-factor)); --bar-padding: calc(4 * var(--space-factor));
padding-top: #{"max(var(--bar-padding), var(--sat,0))"};
padding-right: var(--sar, 0);
padding-bottom: var(--sab, 0);
padding-left: var(--sal, 0);
z-index: 4; z-index: 4;
display: flex; display: flex;
align-items: flex-end; align-items: flex-end;
@ -263,10 +259,6 @@ body.excalidraw-cursor-resize * {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
pointer-events: var(--ui-pointerEvents); pointer-events: var(--ui-pointerEvents);
.panelColumn {
padding: 8px 8px 0 8px;
}
} }
} }
@ -302,6 +294,10 @@ body.excalidraw-cursor-resize * {
overflow-y: auto; overflow-y: auto;
box-sizing: border-box; box-sizing: border-box;
margin-bottom: var(--bar-padding); margin-bottom: var(--bar-padding);
.selected-shape-actions {
padding: 8px 8px 0 8px;
}
} }
.App-menu { .App-menu {

View File

@ -1,7 +1,7 @@
import { import {
DEFAULT_FILENAME, DEFAULT_FILENAME,
EXPORT_DATA_TYPES, EXPORT_DATA_TYPES,
EXPORT_SOURCE, getExportSource,
MIME_TYPES, MIME_TYPES,
VERSIONS, VERSIONS,
} from "@excalidraw/common"; } from "@excalidraw/common";
@ -56,7 +56,7 @@ export const serializeAsJSON = (
const data: ExportedDataState = { const data: ExportedDataState = {
type: EXPORT_DATA_TYPES.excalidraw, type: EXPORT_DATA_TYPES.excalidraw,
version: VERSIONS.excalidraw, version: VERSIONS.excalidraw,
source: EXPORT_SOURCE, source: getExportSource(),
elements: elements:
type === "local" type === "local"
? clearElementsForExport(elements) ? clearElementsForExport(elements)
@ -142,7 +142,7 @@ export const serializeLibraryAsJSON = (libraryItems: LibraryItems) => {
const data: ExportedLibraryData = { const data: ExportedLibraryData = {
type: EXPORT_DATA_TYPES.excalidrawLibrary, type: EXPORT_DATA_TYPES.excalidrawLibrary,
version: VERSIONS.excalidrawLibrary, version: VERSIONS.excalidrawLibrary,
source: EXPORT_SOURCE, source: getExportSource(),
libraryItems, libraryItems,
}; };
return JSON.stringify(data, null, 2); return JSON.stringify(data, null, 2);

View File

@ -17,7 +17,7 @@ import { selectGroupsForSelectedElements } from "@excalidraw/element";
import { getContainerElement } from "@excalidraw/element"; import { getContainerElement } from "@excalidraw/element";
import { arrayToMap, easeOut } from "@excalidraw/common"; import { arrayToMap, easeOut, isShallowEqual } from "@excalidraw/common";
import type { import type {
ExcalidrawElement, ExcalidrawElement,
@ -33,11 +33,18 @@ import { getLassoSelectedElementIds } from "./utils";
import type App from "../components/App"; import type App from "../components/App";
type CanvasTranslate = {
scrollX: number;
scrollY: number;
zoom: number;
};
export class LassoTrail extends AnimatedTrail { export class LassoTrail extends AnimatedTrail {
private intersectedElements: Set<ExcalidrawElement["id"]> = new Set(); private intersectedElements: Set<ExcalidrawElement["id"]> = new Set();
private enclosedElements: Set<ExcalidrawElement["id"]> = new Set(); private enclosedElements: Set<ExcalidrawElement["id"]> = new Set();
private elementsSegments: Map<string, LineSegment<GlobalPoint>[]> | null = private elementsSegments: Map<string, LineSegment<GlobalPoint>[]> | null =
null; null;
private canvasTranslate: CanvasTranslate | null = null;
private keepPreviousSelection: boolean = false; private keepPreviousSelection: boolean = false;
constructor(animationFrameHandler: AnimationFrameHandler, app: App) { constructor(animationFrameHandler: AnimationFrameHandler, app: App) {
@ -169,7 +176,17 @@ export class LassoTrail extends AnimatedTrail {
.getCurrentTrail() .getCurrentTrail()
?.originalPoints?.map((p) => pointFrom<GlobalPoint>(p[0], p[1])); ?.originalPoints?.map((p) => pointFrom<GlobalPoint>(p[0], p[1]));
if (!this.elementsSegments) { const currentCanvasTranslate: CanvasTranslate = {
scrollX: this.app.state.scrollX,
scrollY: this.app.state.scrollY,
zoom: this.app.state.zoom.value,
};
if (
!this.elementsSegments ||
!isShallowEqual(currentCanvasTranslate, this.canvasTranslate ?? {})
) {
this.canvasTranslate = currentCanvasTranslate;
this.elementsSegments = new Map(); this.elementsSegments = new Map();
const visibleElementsMap = arrayToMap(this.app.visibleElements); const visibleElementsMap = arrayToMap(this.app.visibleElements);
for (const element of this.app.visibleElements) { for (const element of this.app.visibleElements) {

View File

@ -188,7 +188,7 @@ const renderLinkIcon = (
window.devicePixelRatio * appState.zoom.value, window.devicePixelRatio * appState.zoom.value,
window.devicePixelRatio * appState.zoom.value, window.devicePixelRatio * appState.zoom.value,
); );
linkCanvasCacheContext.fillStyle = "#fff"; linkCanvasCacheContext.fillStyle = appState.viewBackgroundColor || "#fff";
linkCanvasCacheContext.fillRect(0, 0, width, height); linkCanvasCacheContext.fillRect(0, 0, width, height);
if (canvasKey === "elementLink") { if (canvasKey === "elementLink") {

View File

@ -170,10 +170,11 @@ export const isSnappingEnabled = ({
}) => { }) => {
if (event) { if (event) {
return ( return (
(app.state.objectsSnapModeEnabled && !event[KEYS.CTRL_OR_CMD]) || app.state.activeTool.type !== "lasso" &&
(!app.state.objectsSnapModeEnabled && ((app.state.objectsSnapModeEnabled && !event[KEYS.CTRL_OR_CMD]) ||
event[KEYS.CTRL_OR_CMD] && (!app.state.objectsSnapModeEnabled &&
!isGridModeEnabled(app)) event[KEYS.CTRL_OR_CMD] &&
!isGridModeEnabled(app)))
); );
} }

View File

@ -1324,7 +1324,7 @@ describe("textWysiwyg", () => {
).toEqual(FONT_FAMILY.Nunito); ).toEqual(FONT_FAMILY.Nunito);
expect( expect(
(h.elements[1] as ExcalidrawTextElementWithContainer).lineHeight, (h.elements[1] as ExcalidrawTextElementWithContainer).lineHeight,
).toEqual(1.35); ).toEqual(1.25);
}); });
describe("should align correctly", () => { describe("should align correctly", () => {

View File

@ -13,7 +13,7 @@
"default": "./dist/prod/index.js" "default": "./dist/prod/index.js"
}, },
"./*": { "./*": {
"types": "./../math/dist/types/math/src/*.d.ts" "types": "./dist/types/math/src/*.d.ts"
} }
}, },
"files": [ "files": [

View File

@ -13,7 +13,7 @@
"default": "./dist/prod/index.js" "default": "./dist/prod/index.js"
}, },
"./*": { "./*": {
"types": "./../utils/dist/types/utils/src/*.d.ts" "types": "./dist/types/utils/src/*.d.ts"
} }
}, },
"files": [ "files": [