From 6ec259a0e71174651bae95d4628138bf6fd68742 Mon Sep 17 00:00:00 2001 From: kj_sh604 Date: Sun, 15 Mar 2026 16:19:35 -0400 Subject: refactor: packages/ --- packages/excalidraw/components/Stats/index.tsx | 434 +++++++++++++++++++++++++ 1 file changed, 434 insertions(+) create mode 100644 packages/excalidraw/components/Stats/index.tsx (limited to 'packages/excalidraw/components/Stats/index.tsx') diff --git a/packages/excalidraw/components/Stats/index.tsx b/packages/excalidraw/components/Stats/index.tsx new file mode 100644 index 0000000..2ccd93c --- /dev/null +++ b/packages/excalidraw/components/Stats/index.tsx @@ -0,0 +1,434 @@ +import { useEffect, useMemo, useState, memo } from "react"; +import { getCommonBounds } from "../../element/bounds"; +import type { NonDeletedExcalidrawElement } from "../../element/types"; +import { t } from "../../i18n"; +import type { + AppClassProperties, + AppState, + ExcalidrawProps, +} from "../../types"; +import { CloseIcon } from "../icons"; +import { Island } from "../Island"; +import throttle from "lodash.throttle"; +import Dimension from "./Dimension"; +import Angle from "./Angle"; +import FontSize from "./FontSize"; +import MultiDimension from "./MultiDimension"; +import { elementsAreInSameGroup } from "../../groups"; +import MultiAngle from "./MultiAngle"; +import MultiFontSize from "./MultiFontSize"; +import Position from "./Position"; +import MultiPosition from "./MultiPosition"; +import Collapsible from "./Collapsible"; +import { useExcalidrawAppState, useExcalidrawSetAppState } from "../App"; +import { getAtomicUnits } from "./utils"; +import { STATS_PANELS } from "../../constants"; +import { isElbowArrow, isImageElement } from "../../element/typeChecks"; +import CanvasGrid from "./CanvasGrid"; +import clsx from "clsx"; + +import "./Stats.scss"; +import { isGridModeEnabled } from "../../snapping"; +import { getUncroppedWidthAndHeight } from "../../element/cropElement"; +import { round } from "@excalidraw/math"; +import { frameAndChildrenSelectedTogether } from "../../frame"; + +interface StatsProps { + app: AppClassProperties; + onClose: () => void; + renderCustomStats: ExcalidrawProps["renderCustomStats"]; +} + +const STATS_TIMEOUT = 50; + +export const Stats = (props: StatsProps) => { + const appState = useExcalidrawAppState(); + const sceneNonce = props.app.scene.getSceneNonce() || 1; + const selectedElements = props.app.scene.getSelectedElements({ + selectedElementIds: appState.selectedElementIds, + includeBoundTextElement: false, + }); + const gridModeEnabled = isGridModeEnabled(props.app); + + return ( + + ); +}; + +const StatsRow = ({ + children, + columns = 1, + heading, + style, + ...rest +}: { + children: React.ReactNode; + columns?: number; + heading?: boolean; + style?: React.CSSProperties; +} & React.HTMLAttributes) => ( +
+ {children} +
+); +StatsRow.displayName = "StatsRow"; + +const StatsRows = ({ + children, + order, + style, + ...rest +}: { + children: React.ReactNode; + order?: number; + style?: React.CSSProperties; +} & React.HTMLAttributes) => ( +
+ {children} +
+); +StatsRows.displayName = "StatsRows"; + +Stats.StatsRow = StatsRow; +Stats.StatsRows = StatsRows; + +export const StatsInner = memo( + ({ + app, + onClose, + renderCustomStats, + selectedElements, + appState, + sceneNonce, + gridModeEnabled, + }: StatsProps & { + sceneNonce: number; + selectedElements: readonly NonDeletedExcalidrawElement[]; + appState: AppState; + gridModeEnabled: boolean; + }) => { + const scene = app.scene; + const elements = scene.getNonDeletedElements(); + const elementsMap = scene.getNonDeletedElementsMap(); + const setAppState = useExcalidrawSetAppState(); + + const singleElement = + selectedElements.length === 1 ? selectedElements[0] : null; + + const multipleElements = + selectedElements.length > 1 ? selectedElements : null; + + const cropMode = + appState.croppingElementId && isImageElement(singleElement); + + const unCroppedDimension = cropMode + ? getUncroppedWidthAndHeight(singleElement) + : null; + + const [sceneDimension, setSceneDimension] = useState<{ + width: number; + height: number; + }>({ + width: 0, + height: 0, + }); + + const throttledSetSceneDimension = useMemo( + () => + throttle((elements: readonly NonDeletedExcalidrawElement[]) => { + const boundingBox = getCommonBounds(elements); + setSceneDimension({ + width: Math.round(boundingBox[2]) - Math.round(boundingBox[0]), + height: Math.round(boundingBox[3]) - Math.round(boundingBox[1]), + }); + }, STATS_TIMEOUT), + [], + ); + + useEffect(() => { + throttledSetSceneDimension(elements); + }, [sceneNonce, elements, throttledSetSceneDimension]); + + useEffect( + () => () => throttledSetSceneDimension.cancel(), + [throttledSetSceneDimension], + ); + + const atomicUnits = useMemo(() => { + return getAtomicUnits(selectedElements, appState); + }, [selectedElements, appState]); + + const _frameAndChildrenSelectedTogether = useMemo(() => { + return frameAndChildrenSelectedTogether(selectedElements); + }, [selectedElements]); + + return ( +
+ +
+

{t("stats.title")}

+
+ {CloseIcon} +
+
+ + {t("stats.generalStats")}} + open={!!(appState.stats.panels & STATS_PANELS.generalStats)} + openTrigger={() => + setAppState((state) => { + return { + stats: { + open: true, + panels: state.stats.panels ^ STATS_PANELS.generalStats, + }, + }; + }) + } + > + + {t("stats.scene")} + +
{t("stats.shapes")}
+
{elements.length}
+
+ +
{t("stats.width")}
+
{sceneDimension.width}
+
+ +
{t("stats.height")}
+
{sceneDimension.height}
+
+ {gridModeEnabled && ( + <> + Canvas + + + + + )} +
+ + {renderCustomStats?.(elements, appState)} +
+ + {!_frameAndChildrenSelectedTogether && selectedElements.length > 0 && ( +
+ {t("stats.elementProperties")}} + open={ + !!(appState.stats.panels & STATS_PANELS.elementProperties) + } + openTrigger={() => + setAppState((state) => { + return { + stats: { + open: true, + panels: + state.stats.panels ^ STATS_PANELS.elementProperties, + }, + }; + }) + } + > + + {singleElement && ( + <> + {cropMode && ( + + {t("labels.unCroppedDimension")} + + )} + + {appState.croppingElementId && + isImageElement(singleElement) && + unCroppedDimension && ( + +
{t("stats.width")}
+
{round(unCroppedDimension.width, 2)}
+
+ )} + + {appState.croppingElementId && + isImageElement(singleElement) && + unCroppedDimension && ( + +
{t("stats.height")}
+
{round(unCroppedDimension.height, 2)}
+
+ )} + + + {appState.croppingElementId + ? t("labels.imageCropping") + : t(`element.${singleElement.type}`)} + + + + + + + + + + + + + + + {!isElbowArrow(singleElement) && ( + + + + )} + + + + + )} + + {multipleElements && ( + <> + {elementsAreInSameGroup(multipleElements) && ( + {t("element.group")} + )} + + +
{t("stats.shapes")}
+
{selectedElements.length}
+
+ + + + + + + + + + + + + + + + + + + + + )} +
+
+
+ )} +
+
+ ); + }, + (prev, next) => { + return ( + prev.sceneNonce === next.sceneNonce && + prev.selectedElements === next.selectedElements && + prev.appState.stats.panels === next.appState.stats.panels && + prev.gridModeEnabled === next.gridModeEnabled && + prev.appState.gridStep === next.appState.gridStep && + prev.appState.croppingElementId === next.appState.croppingElementId + ); + }, +); -- cgit v1.2.3