Merge branch 'master' into mtolmacs/feat/precise-hitboxes

This commit is contained in:
Márk Tolmács 2025-05-17 15:09:35 +02:00 committed by GitHub
commit 49613ad0c3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 393 additions and 404 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

@ -73,7 +73,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";
@ -418,50 +418,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>
); );
}, },
@ -485,38 +487,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>
), ),
}); });
@ -540,35 +544,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>
), ),
}); });
@ -591,35 +597,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>
), ),
}); });
@ -658,63 +666,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>
), ),
}); });
@ -1186,52 +1196,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>
); );
}, },
@ -1274,54 +1286,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>
); );
}, },
@ -1369,32 +1383,38 @@ 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)}
/>
</div>
</fieldset> </fieldset>
); );
}, },
@ -1710,48 +1730,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

@ -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

@ -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;
@ -28,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
@ -56,5 +55,5 @@ export const ButtonIconSelect = <T extends Object>(
</label> </label>
), ),
)} )}
</div> </>
); );

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 {