refactor: optimisations (#3523)

* refactor: replace lodash with native approaches

* refactor(deps): update framer-motion versions

* feat(utilities): add @nextui-org/dom-animation

* refactor(components): load domAnimation dynamically

* refactor(deps): add @nextui-org/dom-animation

* fix(utilities): relocate index.ts

* feat(changeset): framer motion optimization

* chore(deps): bump framer-motion version

* fix(docs): conflict issue

* refactor(hooks): remove the unnecessary this aliasing

* refactor(utilities): remove the unnecessary this aliasing

* chore(docs): remove {} so that it won't be true all the time

* chore(dom-animation): end with new line

* refactor(hooks): use debounce from `@nextui-org/shared-utils`

* chore(deps): add `@nextui-org/shared-utils`

* refactor: move mapKeys logic to `@nextui-org/shared-utils`

* refactor: use `get` from `@nextui-org/shared-utils`

* refactor(docs): use `get` from `@nextui-org/shared-utils`

* refactor(shared-utils): mapKeys

* chore(deps): bump framer-motion version

* chore(deps): remove lodash

* refactor(docs): use intersectionBy from shared-utils

* feat(shared-utils): add intersectionBy

* chore(dom-animation): remove extra blank line

* refactor(shared-utils): revise intersectionBy

* fix(modal): add willChange

* refactor(shared-utils): add comments

* fix: build & tests

---------

Co-authored-by: Junior Garcia <jrgarciadev@gmail.com>
This commit is contained in:
աӄա 2024-11-05 04:47:43 +08:00 committed by GitHub
parent 58a77cb6b9
commit 3f0d81b560
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
61 changed files with 776 additions and 390 deletions

View File

@ -0,0 +1,8 @@
---
"@nextui-org/calendar": patch
"@nextui-org/theme": patch
"@nextui-org/use-infinite-scroll": patch
"@nextui-org/shared-utils": patch
---
replaced lodash with native approaches

View File

@ -0,0 +1,15 @@
---
"@nextui-org/accordion": patch
"@nextui-org/calendar": patch
"@nextui-org/modal": patch
"@nextui-org/navbar": patch
"@nextui-org/popover": patch
"@nextui-org/ripple": patch
"@nextui-org/tooltip": patch
"@nextui-org/theme": patch
"@nextui-org/use-infinite-scroll": patch
"@nextui-org/dom-animation": patch
"@nextui-org/shared-utils": patch
---
framer motion optimization (#3340)

View File

@ -0,0 +1,20 @@
---
"@nextui-org/accordion": patch
"@nextui-org/autocomplete": patch
"@nextui-org/button": patch
"@nextui-org/calendar": patch
"@nextui-org/card": patch
"@nextui-org/dropdown": patch
"@nextui-org/modal": patch
"@nextui-org/navbar": patch
"@nextui-org/popover": patch
"@nextui-org/ripple": patch
"@nextui-org/select": patch
"@nextui-org/snippet": patch
"@nextui-org/tabs": patch
"@nextui-org/tooltip": patch
"@nextui-org/system": patch
"@nextui-org/framer-utils": patch
---
update `framer-motion` versions

View File

@ -23,7 +23,7 @@ import {
} from "@nextui-org/react";
import {ChevronDownIcon, SearchIcon} from "@nextui-org/shared-icons";
import {useCallback, useMemo, useState} from "react";
import {capitalize} from "lodash";
import {capitalize} from "@nextui-org/shared-utils";
import {PlusLinearIcon} from "@/components/icons";
import {VerticalDotsIcon} from "@/components/icons/vertical-dots";

View File

@ -23,7 +23,7 @@ import {
} from "@nextui-org/react";
import {ChevronDownIcon, SearchIcon} from "@nextui-org/shared-icons";
import {useCallback, useMemo, useState} from "react";
import {capitalize} from "lodash";
import {capitalize} from "@nextui-org/shared-utils";
import {PlusLinearIcon} from "@/components/icons";
import {VerticalDotsIcon} from "@/components/icons/vertical-dots";

View File

@ -13,7 +13,7 @@ import {clsx} from "@nextui-org/shared-utils";
import scrollIntoView from "scroll-into-view-if-needed";
import {isAppleDevice, isWebKit} from "@react-aria/utils";
import {create} from "zustand";
import {intersectionBy, isEmpty} from "lodash";
import {isEmpty, intersectionBy} from "@nextui-org/shared-utils";
import {writeStorage, useLocalStorage} from "@rehooks/local-storage";
import {

View File

@ -12,7 +12,6 @@ import {
Skeleton,
} from "@nextui-org/react";
import Link from "next/link";
import {toLower} from "lodash";
import {CodeWindow} from "@/components/code-window";
import {useIsMobile} from "@/hooks/use-media-query";
@ -30,8 +29,8 @@ export const DemoCodeModal: FC<DemoCodeModalProps> = ({isOpen, code, title, subt
const isMobile = useIsMobile();
const lowerTitle = toLower(title);
const fileName = `${toLower(lowerTitle)}.tsx`;
const lowerTitle = title.toLowerCase();
const fileName = `${lowerTitle}.tsx`;
return (
<Modal

View File

@ -1,17 +1,19 @@
import {get} from "lodash";
import {FileCode} from "./types";
const importRegex = /^(import)\s(?!type(of\s|\s)(?!from)).*?$/gm;
const exportDefaultRegex = /export\s+default\s+function\s+\w+\s*\(\s*\)\s*\{/;
export const transformCode = (code: string, imports = {}, compName = "App") => {
export const transformCode = (
code: string,
imports: {[key: string]: any} = {},
compName = "App",
) => {
let cleanedCode = code
.replace(importRegex, (match) => {
// get component name from the match ex. "import { Table } from '@nextui-org/react'"
const componentName = match.match(/\w+/g)?.[1] || "";
const matchingImport = get(imports, componentName);
const matchingImport = imports[componentName];
if (matchingImport) {
// remove the matching import

View File

@ -1,7 +1,7 @@
import React, {forwardRef, useEffect} from "react";
import {clsx, dataAttr, getUniqueID} from "@nextui-org/shared-utils";
import BaseHighlight, {Language, PrismTheme, defaultProps} from "prism-react-renderer";
import {debounce, omit} from "lodash";
import {debounce, omit} from "@nextui-org/shared-utils";
import defaultTheme from "@/libs/prism-theme";

View File

@ -4,7 +4,7 @@ import {commonColors, semanticColors} from "@nextui-org/theme";
import {useClipboard} from "@nextui-org/use-clipboard";
import {useState} from "react";
import {useTheme} from "next-themes";
import {get, isEmpty} from "lodash";
import {get, isEmpty} from "@nextui-org/shared-utils";
type ColorsItem = {
color: string;
@ -106,7 +106,7 @@ const SemanticSwatch = ({
let value: string = "";
const [colorName, colorScale] = color.split("-");
let currentPalette = get(semanticColors, theme ?? "", {});
const currentPalette = get(semanticColors, theme ?? "", {});
if (!colorScale) {
value = get(currentPalette, `${colorName}.DEFAULT`, "");

View File

@ -18,7 +18,7 @@ import {
dataFocusVisibleClasses,
} from "@nextui-org/react";
import Link from "next/link";
import {isEmpty} from "lodash";
import {isEmpty} from "@nextui-org/shared-utils";
import {usePathname, useRouter} from "next/navigation";
import {ScrollArea} from "../scroll-area";

View File

@ -3,7 +3,6 @@
/* eslint-disable react/display-name */
import {useMemo, useState} from "react";
import {Tabs, Tab, Card, CardBody, Image, Button, RadioGroup, Radio} from "@nextui-org/react";
import get from "lodash/get";
import NextLink from "next/link";
import NextImage from "next/image";
@ -238,7 +237,7 @@ export const CustomThemes = () => {
<CodeWindow
showWindowIcons
className="max-h-[440px] min-h-[390px]"
highlightLines={get(codeHighlights, selectedTheme)}
highlightLines={codeHighlights[selectedTheme]}
isScrollable={false}
language="jsx"
title="tailwind.config.js"

View File

@ -2,7 +2,7 @@
import {FC, useMemo, useRef} from "react";
import {Avatar, AvatarProps, Button, Spacer, Tooltip} from "@nextui-org/react";
import {clamp, get} from "lodash";
import {clamp} from "@nextui-org/shared-utils";
import {sectionWrapper, titleWrapper, title, subtitle} from "../primitives";
@ -132,9 +132,7 @@ export const Support: FC<SupportProps> = ({sponsors = []}) => {
size={getSponsorSize(sponsor, isMobile)}
src={sponsor.image}
style={getSponsorAvatarStyles(index, sponsors)}
onClick={() =>
handleExternalLinkClick(get(sponsor, "website") || get(sponsor, "profile"))
}
onClick={() => handleExternalLinkClick(sponsor["website"] || sponsor["profile"])}
/>
))}
</div>

View File

@ -24,7 +24,6 @@ import {isAppleDevice} from "@react-aria/utils";
import {clsx} from "@nextui-org/shared-utils";
import NextLink from "next/link";
import {usePathname} from "next/navigation";
import {includes} from "lodash";
import {motion, AnimatePresence} from "framer-motion";
import {useEffect} from "react";
import {usePress} from "@react-aria/interactions";
@ -197,7 +196,7 @@ export const Navbar: FC<NavbarProps> = ({children, routes, mobileRoutes = [], sl
<NextLink
className={navLinkClasses}
color="foreground"
data-active={includes(docsPaths, pathname)}
data-active={docsPaths.includes(pathname)}
href="/docs/guide/introduction"
onClick={() => handlePressNavbarItem("Docs", "/docs/guide/introduction")}
>
@ -208,7 +207,7 @@ export const Navbar: FC<NavbarProps> = ({children, routes, mobileRoutes = [], sl
<NextLink
className={navLinkClasses}
color="foreground"
data-active={includes(pathname, "components")}
data-active={pathname.includes("components")}
href="/docs/components/accordion"
onClick={() => handlePressNavbarItem("Components", "/docs/components/accordion")}
>
@ -219,7 +218,7 @@ export const Navbar: FC<NavbarProps> = ({children, routes, mobileRoutes = [], sl
<NextLink
className={navLinkClasses}
color="foreground"
data-active={includes(pathname, "blog")}
data-active={pathname.includes("blog")}
href="/blog"
onClick={() => handlePressNavbarItem("Blog", "/blog")}
>
@ -230,7 +229,7 @@ export const Navbar: FC<NavbarProps> = ({children, routes, mobileRoutes = [], sl
<NextLink
className={navLinkClasses}
color="foreground"
data-active={includes(pathname, "figma")}
data-active={pathname.includes("figma")}
href="/figma"
onClick={() => handlePressNavbarItem("Figma", "/figma")}
>

View File

@ -1,7 +1,7 @@
import React from "react";
import {usePathname} from "next/navigation";
import {Tooltip, Button} from "@nextui-org/react";
import {capitalize, last} from "lodash";
import {capitalize} from "@nextui-org/shared-utils";
import {BugIcon} from "@/components/icons";
import {ISSUE_REPORT_URL} from "@/libs/github/constants";
@ -9,7 +9,7 @@ import {ISSUE_REPORT_URL} from "@/libs/github/constants";
export const BugReportButton = () => {
const pathname = usePathname();
const componentTitle = capitalize(last(pathname?.split("/")));
const componentTitle = capitalize(pathname?.split("/")?.at(-1) ?? "");
const handlePress = () => {
window.open(`${ISSUE_REPORT_URL}${componentTitle}`, "_blank");

View File

@ -70,7 +70,7 @@ export const useSandpack = ({
}, {});
let dependencies = {
"framer-motion": "11.0.22",
"framer-motion": "11.9.0",
"@nextui-org/react": "latest",
};
@ -143,7 +143,7 @@ export const useSandpack = ({
// const dependencies = useMemo(() => {
// let deps = {
// "framer-motion": "11.0.22",
// "framer-motion": "11.9.0",
// };
// if (hasComponents) {

View File

@ -220,8 +220,8 @@ const users = [
export {columns, users, statusOptions};`;
const utils = `export function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
const utils = `export function capitalize(s) {
return s ? s.charAt(0).toUpperCase() + s.slice(1).toLowerCase() : "";
}`;
const PlusIcon = `export const PlusIcon = ({size = 24, width, height, ...props}) => (

View File

@ -448,8 +448,8 @@ export type IconSvgProps = SVGProps<SVGSVGElement> & {
size?: number;
};`;
const utils = `export function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
const utils = `export function capitalize(s) {
return s ? s.charAt(0).toUpperCase() + s.slice(1).toLowerCase() : "";
}`;
const PlusIcon = `export const PlusIcon = ({size = 24, width, height, ...props}) => (
@ -544,8 +544,8 @@ const ChevronDownIcon = `export const ChevronDownIcon = ({strokeWidth = 1.5, ...
</svg>
);`;
const utilsTs = `export function capitalize(str: string) {
return str.charAt(0).toUpperCase() + str.slice(1);
const utilsTs = `export function capitalize(s: string) {
return s ? s.charAt(0).toUpperCase() + s.slice(1).toLowerCase() : "";
}`;
const PlusIconTs = `import {IconSvgProps} from "./types";

View File

@ -55,11 +55,10 @@
"color2k": "^2.0.2",
"contentlayer2": "^0.5.1",
"date-fns": "^2.30.0",
"framer-motion": "^11.1.7",
"framer-motion": "11.9.0",
"github-slugger": "^2.0.0",
"gray-matter": "^4.0.3",
"hast-util-to-html": "9.0.3",
"lodash": "^4.17.21",
"marked": "^5.1.0",
"match-sorter": "^6.3.1",
"mini-svg-data-uri": "^1.4.3",
@ -102,7 +101,6 @@
"@react-types/shared": "3.24.1",
"@tailwindcss/typography": "^0.5.9",
"@types/canvas-confetti": "^1.4.2",
"@types/lodash": "^4.14.194",
"@types/marked": "^5.0.0",
"@types/mdx": "^2.0.5",
"@types/node": "20.2.5",

View File

@ -1,4 +1,4 @@
import {uniqBy} from "lodash";
import {uniqBy} from "@nextui-org/shared-utils";
import fetch from "node-fetch";
import {__PROD__} from "./env";

View File

@ -42,7 +42,7 @@
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18",
"framer-motion": ">=10.17.0",
"framer-motion": ">=11.5.6",
"@nextui-org/theme": ">=2.1.0",
"@nextui-org/system": ">=2.0.0"
},
@ -54,6 +54,7 @@
"@nextui-org/framer-utils": "workspace:*",
"@nextui-org/divider": "workspace:*",
"@nextui-org/use-aria-accordion": "workspace:*",
"@nextui-org/dom-animation": "workspace:*",
"@react-aria/interactions": "3.22.2",
"@react-aria/focus": "3.18.2",
"@react-aria/utils": "3.25.2",
@ -69,7 +70,7 @@
"@nextui-org/avatar": "workspace:*",
"@nextui-org/input": "workspace:*",
"@nextui-org/test-utils": "workspace:*",
"framer-motion": "^11.0.22",
"framer-motion": "11.9.0",
"clean-package": "2.2.0",
"react": "^18.0.0",
"react-dom": "^18.0.0"

View File

@ -3,13 +3,15 @@ import type {Variants} from "framer-motion";
import {forwardRef} from "@nextui-org/system";
import {useMemo, ReactNode} from "react";
import {ChevronIcon} from "@nextui-org/shared-icons";
import {AnimatePresence, LazyMotion, domAnimation, m, useWillChange} from "framer-motion";
import {AnimatePresence, LazyMotion, m, useWillChange} from "framer-motion";
import {TRANSITION_VARIANTS} from "@nextui-org/framer-utils";
import {UseAccordionItemProps, useAccordionItem} from "./use-accordion-item";
export interface AccordionItemProps extends UseAccordionItemProps {}
const domAnimation = () => import("@nextui-org/dom-animation").then((res) => res.default);
const AccordionItem = forwardRef<"button", AccordionItemProps>((props, ref) => {
const {
Component,

View File

@ -36,7 +36,7 @@
"peerDependencies": {
"@nextui-org/system": ">=2.0.0",
"@nextui-org/theme": ">=2.1.0",
"framer-motion": ">=10.17.0",
"framer-motion": ">=11.5.6",
"react": ">=18",
"react-dom": ">=18"
},
@ -72,7 +72,7 @@
"@nextui-org/use-infinite-scroll": "workspace:*",
"@react-stately/data": "3.11.6",
"clean-package": "2.2.0",
"framer-motion": "^11.0.28",
"framer-motion": "11.9.0",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-hook-form": "^7.51.3"

View File

@ -36,7 +36,7 @@
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18",
"framer-motion": ">=10.17.0",
"framer-motion": ">=11.5.6",
"@nextui-org/theme": ">=2.1.0",
"@nextui-org/system": ">=2.0.0"
},

View File

@ -46,7 +46,7 @@
"@nextui-org/framer-utils": "workspace:*",
"@nextui-org/use-aria-button": "workspace:*",
"@nextui-org/button": "workspace:*",
"lodash.debounce": "^4.0.8",
"@nextui-org/dom-animation": "workspace:*",
"@internationalized/date": "3.5.5",
"@react-aria/calendar": "3.5.11",
"@react-aria/focus": "3.18.2",
@ -67,7 +67,7 @@
"@nextui-org/theme": "workspace:*",
"@nextui-org/radio": "workspace:*",
"@nextui-org/test-utils": "workspace:*",
"framer-motion": "^10.16.4",
"framer-motion": "11.9.0",
"clean-package": "2.2.0",
"react": "^18.0.0",
"react-dom": "^18.0.0"

View File

@ -3,12 +3,11 @@ import type {As, HTMLNextUIProps} from "@nextui-org/system";
import type {ButtonProps} from "@nextui-org/button";
import type {HTMLAttributes, ReactNode, RefObject} from "react";
import {Fragment} from "react";
import {useState} from "react";
import {Fragment, useState} from "react";
import {VisuallyHidden} from "@react-aria/visually-hidden";
import {Button} from "@nextui-org/button";
import {chain, mergeProps} from "@react-aria/utils";
import {AnimatePresence, LazyMotion, domAnimation, MotionConfig} from "framer-motion";
import {AnimatePresence, LazyMotion, MotionConfig} from "framer-motion";
import {ResizablePanel} from "@nextui-org/framer-utils";
import {ChevronLeftIcon} from "./chevron-left";
@ -19,6 +18,8 @@ import {CalendarHeader} from "./calendar-header";
import {CalendarPicker} from "./calendar-picker";
import {useCalendarContext} from "./calendar-context";
const domAnimation = () => import("@nextui-org/dom-animation").then((res) => res.default);
export interface CalendarBaseProps extends HTMLNextUIProps<"div"> {
Component?: As;
showHelper?: boolean;

View File

@ -4,7 +4,7 @@ import type {PressEvent} from "@react-types/shared";
import {useDateFormatter} from "@react-aria/i18n";
import {HTMLNextUIProps} from "@nextui-org/system";
import {useCallback, useRef, useEffect} from "react";
import debounce from "lodash.debounce";
import {debounce} from "@nextui-org/shared-utils";
import {areRectsIntersecting} from "@nextui-org/react-utils";
import scrollIntoView from "scroll-into-view-if-needed";

View File

@ -36,7 +36,7 @@
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18",
"framer-motion": ">=10.17.0",
"framer-motion": ">=11.5.6",
"@nextui-org/theme": ">=2.1.0",
"@nextui-org/system": ">=2.0.0"
},

View File

@ -36,7 +36,7 @@
"peerDependencies": {
"@nextui-org/system": ">=2.0.0",
"@nextui-org/theme": ">=2.1.0",
"framer-motion": ">=10.17.0",
"framer-motion": ">=11.5.6",
"react": ">=18",
"react-dom": ">=18"
},
@ -62,7 +62,7 @@
"@nextui-org/theme": "workspace:*",
"@nextui-org/user": "workspace:*",
"clean-package": "2.2.0",
"framer-motion": "^11.0.22",
"framer-motion": "11.9.0",
"react": "^18.0.0",
"react-dom": "^18.0.0"
},

View File

@ -36,7 +36,7 @@
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18",
"framer-motion": ">=10.17.0",
"framer-motion": ">=11.5.6",
"@nextui-org/theme": ">=2.1.0",
"@nextui-org/system": ">=2.0.0"
},
@ -49,6 +49,7 @@
"@nextui-org/react-utils": "workspace:*",
"@nextui-org/shared-icons": "workspace:*",
"@nextui-org/use-aria-modal-overlay": "workspace:*",
"@nextui-org/dom-animation": "workspace:*",
"@react-aria/dialog": "3.5.17",
"@react-aria/focus": "3.18.2",
"@react-aria/interactions": "3.22.2",
@ -66,7 +67,7 @@
"@nextui-org/link": "workspace:*",
"@nextui-org/switch": "workspace:*",
"react-lorem-component": "0.13.0",
"framer-motion": "^11.0.22",
"framer-motion": "11.9.0",
"clean-package": "2.2.0",
"react": "^18.0.0",
"react-dom": "^18.0.0"

View File

@ -6,7 +6,7 @@ import {forwardRef} from "@nextui-org/system";
import {DismissButton} from "@react-aria/overlays";
import {TRANSITION_VARIANTS} from "@nextui-org/framer-utils";
import {CloseIcon} from "@nextui-org/shared-icons";
import {domAnimation, LazyMotion, m} from "framer-motion";
import {LazyMotion, m} from "framer-motion";
import {useDialog} from "@react-aria/dialog";
import {chain, mergeProps, useViewportSize} from "@react-aria/utils";
import {HTMLNextUIProps} from "@nextui-org/system";
@ -21,6 +21,8 @@ export interface ModalContentProps extends AriaDialogProps, HTMLNextUIProps<"div
children: ReactNode | ((onClose: () => void) => ReactNode);
}
const domAnimation = () => import("@nextui-org/dom-animation").then((res) => res.default);
const ModalContent = forwardRef<"div", ModalContentProps, KeysToOmit>((props, _) => {
const {as, children, role = "dialog", ...otherProps} = props;

View File

@ -5,6 +5,7 @@ export const scaleInOut = {
scale: "var(--scale-enter)",
y: "var(--slide-enter)",
opacity: 1,
willChange: "auto",
transition: {
scale: {
duration: 0.4,
@ -25,6 +26,7 @@ export const scaleInOut = {
scale: "var(--scale-exit)",
y: "var(--slide-exit)",
opacity: 0,
willChange: "transform",
transition: {
duration: 0.3,
ease: TRANSITION_EASINGS.ease,

View File

@ -36,7 +36,7 @@
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18",
"framer-motion": ">=10.17.0",
"framer-motion": ">=11.5.6",
"@nextui-org/theme": ">=2.1.0",
"@nextui-org/system": ">=2.0.0"
},
@ -46,6 +46,7 @@
"@nextui-org/framer-utils": "workspace:*",
"@nextui-org/use-aria-toggle-button": "workspace:*",
"@nextui-org/use-scroll-position": "workspace:*",
"@nextui-org/dom-animation": "workspace:*",
"@react-aria/focus": "3.18.2",
"@react-aria/interactions": "3.22.2",
"@react-aria/overlays": "3.23.2",

View File

@ -1,7 +1,7 @@
import {forwardRef, HTMLNextUIProps} from "@nextui-org/system";
import {useDOMRef} from "@nextui-org/react-utils";
import {clsx, dataAttr} from "@nextui-org/shared-utils";
import {AnimatePresence, domAnimation, HTMLMotionProps, LazyMotion, m} from "framer-motion";
import {AnimatePresence, HTMLMotionProps, LazyMotion, m} from "framer-motion";
import {mergeProps} from "@react-aria/utils";
import {ReactElement, useCallback} from "react";
import {RemoveScroll} from "react-remove-scroll";
@ -23,6 +23,8 @@ export interface NavbarMenuProps extends HTMLNextUIProps<"ul"> {
motionProps?: HTMLMotionProps<"ul">;
}
const domAnimation = () => import("@nextui-org/dom-animation").then((res) => res.default);
const NavbarMenu = forwardRef<"ul", NavbarMenuProps>((props, ref) => {
const {className, children, portalContainer, motionProps, style, ...otherProps} = props;
const domRef = useDOMRef(ref);

View File

@ -1,6 +1,6 @@
import {forwardRef} from "@nextui-org/system";
import {pickChildren} from "@nextui-org/react-utils";
import {LazyMotion, domAnimation, m} from "framer-motion";
import {LazyMotion, m} from "framer-motion";
import {mergeProps} from "@react-aria/utils";
import {hideOnScrollVariants} from "./navbar-transitions";
@ -12,6 +12,8 @@ export interface NavbarProps extends Omit<UseNavbarProps, "hideOnScroll"> {
children?: React.ReactNode | React.ReactNode[];
}
const domAnimation = () => import("@nextui-org/dom-animation").then((res) => res.default);
const Navbar = forwardRef<"div", NavbarProps>((props, ref) => {
const {children, ...otherProps} = props;

View File

@ -36,7 +36,7 @@
"peerDependencies": {
"@nextui-org/system": ">=2.0.0",
"@nextui-org/theme": ">=2.1.0",
"framer-motion": ">=10.17.0",
"framer-motion": ">=11.5.6",
"react": ">=18",
"react-dom": ">=18"
},
@ -48,6 +48,7 @@
"@nextui-org/shared-utils": "workspace:*",
"@nextui-org/use-aria-button": "workspace:*",
"@nextui-org/use-safe-layout-effect": "workspace:*",
"@nextui-org/dom-animation": "workspace:*",
"@react-aria/dialog": "3.5.17",
"@react-aria/focus": "3.18.2",
"@react-aria/interactions": "3.22.2",
@ -64,7 +65,7 @@
"@nextui-org/system": "workspace:*",
"@nextui-org/theme": "workspace:*",
"clean-package": "2.2.0",
"framer-motion": "^11.0.22",
"framer-motion": "11.9.0",
"react": "^18.0.0",
"react-dom": "^18.0.0"
},

View File

@ -10,7 +10,7 @@
import * as React from "react";
import {DismissButton, Overlay} from "@react-aria/overlays";
import {forwardRef} from "@nextui-org/system";
import {domAnimation, HTMLMotionProps, LazyMotion, m} from "framer-motion";
import {HTMLMotionProps, LazyMotion, m} from "framer-motion";
import {mergeProps} from "@react-aria/utils";
import {getTransformOrigins} from "@nextui-org/aria-utils";
import {TRANSITION_VARIANTS} from "@nextui-org/framer-utils";
@ -18,6 +18,8 @@ import {useDialog} from "@react-aria/dialog";
import {usePopover, UsePopoverProps, UsePopoverReturn} from "./use-popover";
const domAnimation = () => import("@nextui-org/dom-animation").then((res) => res.default);
export interface FreeSoloPopoverProps extends Omit<UsePopoverProps, "children"> {
children: React.ReactNode | ((titleProps: React.DOMAttributes<HTMLElement>) => React.ReactNode);
transformOrigin?: {

View File

@ -6,7 +6,7 @@ import {forwardRef} from "@nextui-org/system";
import {RemoveScroll} from "react-remove-scroll";
import {DismissButton} from "@react-aria/overlays";
import {TRANSITION_VARIANTS} from "@nextui-org/framer-utils";
import {m, domAnimation, LazyMotion} from "framer-motion";
import {m, LazyMotion} from "framer-motion";
import {HTMLNextUIProps} from "@nextui-org/system";
import {getTransformOrigins} from "@nextui-org/aria-utils";
import {useDialog} from "@react-aria/dialog";
@ -19,6 +19,8 @@ export interface PopoverContentProps
children: ReactNode | ((titleProps: DOMAttributes<HTMLElement>) => ReactNode);
}
const domAnimation = () => import("@nextui-org/dom-animation").then((res) => res.default);
const PopoverContent = forwardRef<"div", PopoverContentProps>((props, _) => {
const {as, children, className, ...otherProps} = props;

View File

@ -36,19 +36,20 @@
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18",
"framer-motion": ">=10.17.0",
"framer-motion": ">=11.5.6",
"@nextui-org/theme": ">=2.1.0",
"@nextui-org/system": ">=2.0.0"
},
"dependencies": {
"@nextui-org/shared-utils": "workspace:*",
"@nextui-org/react-utils": "workspace:*"
"@nextui-org/react-utils": "workspace:*",
"@nextui-org/dom-animation": "workspace:*"
},
"devDependencies": {
"@nextui-org/theme": "workspace:*",
"@nextui-org/system": "workspace:*",
"clean-package": "2.2.0",
"framer-motion": "^11.0.22",
"framer-motion": "11.9.0",
"react": "^18.0.0",
"react-dom": "^18.0.0"
},

View File

@ -3,7 +3,7 @@ import type {FC} from "react";
import type {HTMLMotionProps} from "framer-motion";
import type {HTMLNextUIProps} from "@nextui-org/system";
import {AnimatePresence, m, LazyMotion, domAnimation} from "framer-motion";
import {AnimatePresence, m, LazyMotion} from "framer-motion";
import {clamp} from "@nextui-org/shared-utils";
export interface RippleProps extends HTMLNextUIProps<"span"> {
@ -14,6 +14,8 @@ export interface RippleProps extends HTMLNextUIProps<"span"> {
onClear: (key: React.Key) => void;
}
const domAnimation = () => import("@nextui-org/dom-animation").then((res) => res.default);
const Ripple: FC<RippleProps> = (props) => {
const {ripples = [], motionProps, color = "currentColor", style, onClear} = props;

View File

@ -36,7 +36,7 @@
"peerDependencies": {
"@nextui-org/system": ">=2.0.0",
"@nextui-org/theme": ">=2.1.0",
"framer-motion": ">=10.17.0",
"framer-motion": ">=11.5.6",
"react": ">=18",
"react-dom": ">=18"
},
@ -71,7 +71,7 @@
"@react-aria/i18n": "3.12.2",
"@react-stately/data": "3.11.6",
"clean-package": "2.2.0",
"framer-motion": "^11.0.28",
"framer-motion": "11.9.0",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-hook-form": "^7.51.3"

View File

@ -36,7 +36,7 @@
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18",
"framer-motion": ">=10.17.0",
"framer-motion": ">=11.5.6",
"@nextui-org/theme": ">=2.1.0",
"@nextui-org/system": ">=2.0.0"
},

View File

@ -36,7 +36,7 @@
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18",
"framer-motion": ">=10.17.0",
"framer-motion": ">=11.5.6",
"@nextui-org/theme": ">=2.1.0",
"@nextui-org/system": ">=2.0.0"
},
@ -59,7 +59,7 @@
"devDependencies": {
"@nextui-org/theme": "workspace:*",
"@nextui-org/system": "workspace:*",
"framer-motion": "^11.0.22",
"framer-motion": "11.9.0",
"react-lorem-component": "0.13.0",
"@nextui-org/card": "workspace:*",
"@nextui-org/input": "workspace:*",

View File

@ -123,6 +123,8 @@ const Tab = forwardRef<"button", TabItemProps>((props, ref) => {
type={Component === "button" ? "button" : undefined}
>
{isSelected && !disableAnimation && !disableCursorAnimation && isMounted ? (
// use synchronous loading for domMax here
// since lazy loading produces different behaviour
<LazyMotion features={domMax}>
<m.span
className={slots.cursor({class: classNames?.cursor})}

View File

@ -36,7 +36,7 @@
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18",
"framer-motion": ">=10.17.0",
"framer-motion": ">=11.5.6",
"@nextui-org/theme": ">=2.1.0",
"@nextui-org/system": ">=2.0.0"
},
@ -46,6 +46,7 @@
"@nextui-org/aria-utils": "workspace:*",
"@nextui-org/framer-utils": "workspace:*",
"@nextui-org/use-safe-layout-effect": "workspace:*",
"@nextui-org/dom-animation": "workspace:*",
"@react-aria/interactions": "3.22.2",
"@react-aria/overlays": "3.23.2",
"@react-aria/tooltip": "3.7.7",
@ -59,7 +60,7 @@
"@nextui-org/system": "workspace:*",
"@nextui-org/theme": "workspace:*",
"clean-package": "2.2.0",
"framer-motion": "^11.0.28",
"framer-motion": "11.9.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},

View File

@ -1,6 +1,6 @@
import {forwardRef} from "@nextui-org/system";
import {OverlayContainer} from "@react-aria/overlays";
import {AnimatePresence, m, LazyMotion, domAnimation} from "framer-motion";
import {AnimatePresence, m, LazyMotion} from "framer-motion";
import {TRANSITION_VARIANTS} from "@nextui-org/framer-utils";
import {warn} from "@nextui-org/shared-utils";
import {Children, cloneElement, isValidElement} from "react";
@ -11,6 +11,8 @@ import {UseTooltipProps, useTooltip} from "./use-tooltip";
export interface TooltipProps extends Omit<UseTooltipProps, "disableTriggerFocus" | "backdrop"> {}
const domAnimation = () => import("@nextui-org/dom-animation").then((res) => res.default);
const Tooltip = forwardRef<"div", TooltipProps>((props, ref) => {
const {
Component,

View File

@ -91,7 +91,7 @@
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18",
"framer-motion": ">=10.17.0"
"framer-motion": ">=11.5.6"
},
"devDependencies": {
"react": "^18.0.0",

View File

@ -34,13 +34,13 @@
"postpack": "clean-package restore"
},
"peerDependencies": {
"framer-motion": ">=10.17.0",
"framer-motion": ">=11.5.6",
"react": ">=18",
"react-dom": ">=18"
},
"devDependencies": {
"clean-package": "2.2.0",
"framer-motion": "^11.0.22",
"framer-motion": "11.9.0",
"react": "^18.0.0",
"react-dom": "^18.0.0"
},

View File

@ -1,5 +1,5 @@
import {getContrast} from "color2k";
import get from "lodash.get";
import {get} from "@nextui-org/shared-utils";
import {semanticColors} from "../src/colors/semantic";

View File

@ -50,14 +50,10 @@
"color2k": "^2.0.2",
"deepmerge": "4.3.1",
"flat": "^5.0.2",
"lodash.foreach": "^4.5.0",
"lodash.get": "^4.4.2",
"lodash.kebabcase": "^4.1.1",
"lodash.mapkeys": "^4.6.0",
"lodash.omit": "^4.5.0",
"clsx": "^1.2.1",
"tailwind-variants": "^0.1.20",
"tailwind-merge": "^2.5.2"
"tailwind-merge": "^2.5.2",
"@nextui-org/shared-utils": "workspace:*"
},
"peerDependencies": {
"tailwindcss": ">=3.4.0"
@ -65,11 +61,6 @@
"devDependencies": {
"@types/color": "^3.0.3",
"@types/flat": "^5.0.2",
"@types/lodash.foreach": "^4.5.7",
"@types/lodash.get": "^4.4.7",
"@types/lodash.kebabcase": "^4.1.7",
"@types/lodash.mapkeys": "^4.6.7",
"@types/lodash.omit": "^4.5.7",
"tailwindcss": "^3.4.0",
"clean-package": "2.2.0"
},

View File

@ -5,12 +5,8 @@
import Color from "color";
import plugin from "tailwindcss/plugin.js";
import get from "lodash.get";
import omit from "lodash.omit";
import forEach from "lodash.foreach";
import mapKeys from "lodash.mapkeys";
import kebabCase from "lodash.kebabcase";
import deepMerge from "deepmerge";
import {omit, kebabCase, mapKeys} from "@nextui-org/shared-utils";
import {semanticColors, commonColors} from "./colors";
import {animations} from "./animations";
@ -63,7 +59,7 @@ const resolveConfig = (
// flatten color definitions
const flatColors = flattenThemeObject(colors) as Record<string, string>;
const flatLayout = layout ? mapKeys(layout, (value, key) => kebabCase(key)) : {};
const flatLayout = layout ? mapKeys(layout, (_, key) => kebabCase(key)) : {};
// resolved.variants
resolved.variants.push({
@ -246,8 +242,8 @@ export const nextui = (config: NextUIPluginConfig = {}): ReturnType<typeof plugi
addCommonColors = false,
} = config;
const userLightColors = get(themeObject, "light.colors", {});
const userDarkColors = get(themeObject, "dark.colors", {});
const userLightColors = themeObject?.light?.colors || {};
const userDarkColors = themeObject?.dark?.colors || {};
const defaultLayoutObj =
userLayout && typeof userLayout === "object"
@ -268,7 +264,7 @@ export const nextui = (config: NextUIPluginConfig = {}): ReturnType<typeof plugi
// get other themes from the config different from light and dark
let otherThemes = omit(themeObject, ["light", "dark"]) || {};
forEach(otherThemes, ({extend, colors, layout}, themeName) => {
Object.entries(otherThemes).forEach(([themeName, {extend, colors, layout}]) => {
const baseTheme = extend && isBaseTheme(extend) ? extend : defaultExtendTheme;
if (colors && typeof colors === "object") {
@ -283,12 +279,12 @@ export const nextui = (config: NextUIPluginConfig = {}): ReturnType<typeof plugi
});
const light: ConfigTheme = {
layout: deepMerge(baseLayouts.light, get(themeObject, "light.layout", {})),
layout: deepMerge(baseLayouts.light, themeObject?.light?.layout || {}),
colors: deepMerge(semanticColors.light, userLightColors),
};
const dark = {
layout: deepMerge(baseLayouts.dark, get(themeObject, "dark.layout", {})),
layout: deepMerge(baseLayouts.dark, themeObject?.dark?.layout || {}),
colors: deepMerge(semanticColors.dark, userDarkColors),
};

View File

@ -33,13 +33,12 @@
"prepack": "clean-package",
"postpack": "clean-package restore"
},
"dependencies": {
"lodash.debounce": "^4.0.8",
"@types/lodash.debounce": "^4.0.7"
},
"peerDependencies": {
"react": ">=18"
},
"dependencies": {
"@nextui-org/shared-utils": "workspace:*"
},
"devDependencies": {
"clean-package": "2.2.0",
"react": "^18.0.0"

View File

@ -1,5 +1,5 @@
import debounce from "lodash.debounce";
import {useLayoutEffect, useRef, useCallback} from "react";
import {debounce} from "@nextui-org/shared-utils";
export interface UseInfiniteScrollProps {
/**

View File

@ -0,0 +1,23 @@
# @nextui-org/dom-animation
A Quick description of the component
> This is an internal utility, not intended for public usage.
## Installation
```sh
yarn add @nextui-org/dom-animation
# or
npm i @nextui-org/dom-animation
```
## Contribution
Yes please! See the
[contributing guidelines](https://github.com/nextui-org/nextui/blob/master/CONTRIBUTING.md)
for details.
## License
This project is licensed under the terms of the
[MIT license](https://github.com/nextui-org/nextui/blob/master/LICENSE).

View File

@ -0,0 +1,44 @@
{
"name": "@nextui-org/dom-animation",
"version": "2.0.0",
"description": "Dom Animation from Framer Motion for dynamic import",
"keywords": [
"dom-animation"
],
"author": "WK Wong <wingkwong.code@gmail.com>",
"homepage": "https://nextui.org",
"license": "MIT",
"main": "src/index.ts",
"sideEffects": false,
"files": [
"dist"
],
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/nextui-org/nextui.git",
"directory": "packages/utilities/dom-animation"
},
"bugs": {
"url": "https://github.com/nextui-org/nextui/issues"
},
"scripts": {
"build": "tsup src --dts",
"build:fast": "tsup src",
"dev": "pnpm build:fast --watch",
"clean": "rimraf dist .turbo",
"typecheck": "tsc --noEmit",
"prepack": "clean-package",
"postpack": "clean-package restore"
},
"peerDependencies": {
"framer-motion": ">=11.5.6"
},
"devDependencies": {
"clean-package": "2.2.0",
"framer-motion": "11.9.0"
},
"clean-package": "../../../clean-package.config.json"
}

View File

@ -0,0 +1,3 @@
import {domAnimation} from "framer-motion";
export default domAnimation;

View File

@ -0,0 +1,4 @@
{
"extends": "../../../tsconfig.json",
"include": ["src", "index.ts"]
}

View File

@ -0,0 +1,8 @@
import {defineConfig} from "tsup";
export default defineConfig({
clean: true,
target: "es2019",
format: ["cjs", "esm"],
banner: {js: '"use client";'},
});

View File

@ -36,7 +36,7 @@
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18",
"framer-motion": ">=10.17.0"
"framer-motion": ">=11.5.6"
},
"dependencies": {
"@nextui-org/system": "workspace:*",
@ -47,7 +47,7 @@
"react": "^18.0.0",
"react-dom": "^18.0.0",
"clean-package": "2.2.0",
"framer-motion": "^11.0.22"
"framer-motion": "11.9.0"
},
"clean-package": "../../../clean-package.config.json"
}

View File

@ -8,15 +8,37 @@ type Extractable =
}
| undefined;
type Iteratee<T> = ((value: T) => any) | keyof T;
/**
* Capitalizes the first letter of a string
* @param {string} text
* @returns {string}
* Capitalizes the first character of a string and converts the rest of the string to lowercase.
*
* @param s - The string to capitalize.
* @returns The capitalized string, or an empty string if the input is falsy.
*
* @example
* capitalize('hello'); // returns 'Hello'
* capitalize(''); // returns ''
*/
export const capitalize = (text: string) => {
return text.charAt(0).toUpperCase() + text.slice(1);
export const capitalize = (s: string) => {
return s ? s.charAt(0).toUpperCase() + s.slice(1).toLowerCase() : "";
};
/**
* Creates a function that invokes each provided function with the same argument, until
* one of the functions calls `event.preventDefault()`.
*
* @param fns - An array of functions that may or may not be defined.
* @returns A function that takes an event and invokes each handler with this event.
*
* @typeParam T - A function type that takes an event-like argument.
*
* @example
* const handler1 = event => console.log('Handled by first', event.type);
* const handler2 = event => event.preventDefault();
* const allHandlers = callAllHandlers(handler1, handler2);
* allHandlers({ type: 'click' });
*/
export function callAllHandlers<T extends (event: any) => void>(...fns: (T | undefined)[]) {
return function func(event: Args<T>[0]) {
fns.some((fn) => {
@ -27,6 +49,20 @@ export function callAllHandlers<T extends (event: any) => void>(...fns: (T | und
};
}
/**
* Creates a function that invokes each provided function with the same argument.
*
* @param fns - An array of functions that may or may not be defined.
* @returns A function that takes one argument and invokes all provided functions with it.
*
* @typeParam T - A function type that takes any argument.
*
* @example
* const greet = name => console.log(`Hello, ${name}!`);
* const bye = name => console.log(`Goodbye, ${name}!`);
* const greetAndBye = callAll(greet, bye);
* greetAndBye('Alice');
*/
export function callAll<T extends AnyFunction>(...fns: (T | undefined)[]) {
return function mergedFn(arg: Args<T>[0]) {
fns.forEach((fn) => {
@ -35,6 +71,21 @@ export function callAll<T extends AnyFunction>(...fns: (T | undefined)[]) {
};
}
/**
* Extracts a property from a list of objects, returning the first found non-falsy value or a default value.
*
* @param key - The key of the property to extract.
* @param defaultValue - The default value to return if no non-falsy property value is found.
* @param objs - An array of objects to search.
* @returns The value of the extracted property or the default value.
*
* @typeParam K - The type of the key.
* @typeParam D - The type of the default value.
*
* @example
* extractProperty('name', 'Unknown', { name: 'Alice' }, { name: 'Bob' }); // returns 'Alice'
* extractProperty('age', 18, { name: 'Alice' }); // returns 18
*/
export function extractProperty<K extends keyof Extractable, D extends keyof Extractable>(
key: K | string,
defaultValue: D | string | boolean,
@ -51,12 +102,27 @@ export function extractProperty<K extends keyof Extractable, D extends keyof Ext
return result as Extractable[K] | D | string | boolean;
}
/**
* Generates a unique identifier using a specified prefix and a random number.
*
* @param prefix - The prefix to prepend to the unique identifier.
* @returns A string that combines the prefix and a random number.
*
* @example
* getUniqueID('btn'); // returns 'btn-123456'
*/
export function getUniqueID(prefix: string) {
return `${prefix}-${Math.floor(Math.random() * 1000000)}`;
}
/**
* This function removes all event handlers from an object.
* Removes all properties from an object that start with 'on', which are typically event handlers.
*
* @param input - The object from which to remove event properties.
* @returns The same object with event properties removed.
*
* @example
* removeEvents({ onClick: () => {}, onChange: () => {}, value: 10 }); // returns { value: 10 }
*/
export function removeEvents(input: {[key: string]: any}) {
for (const key in input) {
@ -68,6 +134,18 @@ export function removeEvents(input: {[key: string]: any}) {
return input;
}
/**
* Converts an object into a JSON string. Returns an empty string if the object
* is not extractable or if a circular reference is detected during stringification.
*
* @param obj - The object to convert into a dependency string.
*
* @returns A JSON string representation of the object or an empty string if conversion fails.
*
* @example
* objectToDeps({ key: 'value' }); // returns '{"key":"value"}'
* objectToDeps(undefined); // returns ""
*/
export function objectToDeps(obj: Extractable) {
if (!obj || typeof obj !== "object") {
return "";
@ -79,3 +157,235 @@ export function objectToDeps(obj: Extractable) {
return "";
}
}
/**
* Creates a debounced function that delays invoking `func` until after `waitMilliseconds` have elapsed
* since the last time the debounced function was invoked. The debounced function has the
* same `this` context and arguments as the original function.
*
* @param func - The function to debounce.
* @param waitMilliseconds - The number of milliseconds to delay; defaults to 0.
*
* @returns A new debounced function.
*
* @typeParam F - The type of the function to debounce.
*
* @example
* const save = debounce(() => console.log('Saved!'), 300);
* save(); // Will log 'Saved!' after 300ms, subsequent calls within 300ms will reset the timer.
*/
export function debounce<F extends (...args: any[]) => void>(
func: F,
waitMilliseconds: number = 0,
) {
let timeout: ReturnType<typeof setTimeout> | undefined;
return function (this: ThisParameterType<F>, ...args: Parameters<F>) {
const later = () => {
timeout = undefined;
func.apply(this, args);
};
if (timeout !== undefined) {
clearTimeout(timeout);
}
timeout = setTimeout(later, waitMilliseconds);
};
}
/**
* Returns a new array of unique elements from the given array, where the uniqueness is determined by the specified iteratee.
*
* @param arr - The array to process.
* @param iteratee - The iteratee invoked per element to generate the criterion by which uniqueness is computed.
* This can be a function or the string key of the object properties.
*
* @returns A new array of elements that are unique based on the iteratee function.
*
* @typeParam T - The type of elements in the input array.
*
* @example
* uniqBy([{ id: 1 }, { id: 2 }, { id: 1 }], 'id'); // returns [{ id: 1 }, { id: 2 }]
*/
export function uniqBy<T>(arr: T[], iteratee: any) {
if (typeof iteratee === "string") {
iteratee = (item: T) => item[iteratee as keyof T];
}
return arr.filter((x, i, self) => i === self.findIndex((y) => iteratee(x) === iteratee(y)));
}
/**
* Creates an object composed of the own and inherited enumerable property paths of `obj` that are not omitted.
*
* @param obj - The source object.
* @param keys - The property keys to omit.
*
* @returns A new object with the keys specified omitted.
*
* @typeParam Obj - The type of the object.
* @typeParam Keys - The type of the keys to omit.
*
* @example
* omit({ a: 1, b: '2', c: 3 }, ['a', 'c']); // returns { b: '2' }
*/
export const omit = <Obj, Keys extends keyof Obj>(obj: Obj, keys: Keys[]): Omit<Obj, Keys> => {
const res = Object.assign({}, obj);
keys.forEach((key) => {
delete res[key];
});
return res;
};
/**
* Converts a string to kebab-case.
*
* @param s - The string to convert.
*
* @returns The kebab-case version of the string.
*
* @example
* kebabCase('fooBar'); // returns 'foo-bar'
*/
export const kebabCase = (s: string) => {
return s.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
};
/**
* Creates an object with keys transformed using provided `iteratee` function, which takes each value and the corresponding key.
*
* @param obj - The source object.
* @param iteratee - The function invoked per iteration to transform the keys.
*
* @returns A new object with keys transformed by `iteratee`.
*
* @example
* mapKeys({ a: 1, b: 2 }, (value, key) => key + value); // returns { a1: 1, b2: 2 }
*/
export const mapKeys = (
obj: Record<string, any>,
iteratee: (value: any, key: string) => any,
): Record<string, any> => {
return Object.fromEntries(
Object.entries(obj).map(([key, value]) => [iteratee(value, key), value]),
);
};
/**
* Retrieves the value at a given path of a provided object safely. If the path does not exist,
* the function returns a default value, if provided.
*
* @param object - The object from which to retrieve the property.
* @param path - A dot notation string or an array of strings and numbers indicating the path of the property in the object.
* Dot notation can also include array indices in bracket notation (e.g., 'a.b[0].c').
* @param defaultValue - The value to return if the resolved value is `undefined`. This parameter is optional.
*
* @returns The value from the object at the specified path, or the default value if the path is not resolved.
*
* @example
* const obj = { a: { b: [{ c: 3 }] } };
*
* // Using string path with dot and bracket notation
* get(obj, 'a.b[0].c'); // returns 3
*
* // Using array path
* get(obj, ['a', 'b', 0, 'c']); // returns 3
*
* // Using default value for non-existent path
* get(obj, 'a.b[1].c', 'not found'); // returns 'not found'
*/
export const get = (
object: Record<string, any>,
path: string | (string | number)[],
defaultValue?: any,
): any => {
const keys = Array.isArray(path) ? path : path.replace(/\[(\d+)\]/g, ".$1").split(".");
let res: any = object;
for (const key of keys) {
res = res?.[key];
if (res === undefined) {
return defaultValue;
}
}
return res;
};
/**
* Computes the list of values that are the intersection of all provided arrays,
* with each element being transformed by the given iteratee, which can be a function or property name.
*
* @param args - A rest parameter that collects all arrays to intersect followed by an iteratee.
* The last element in `args` is the iteratee, which can be either a function or a property name string.
* The rest are arrays of elements of type T.
*
* @returns An array of elements of type T that exist in all arrays after being transformed by the iteratee.
*
* @throws {Error} If less than two arguments are provided or if the iteratee is not a function or a valid property string.
*
* @typeParam T - The type of elements in the input arrays.
*
* @example
* // Using a function as an iteratee
* intersectionBy([2.1, 1.2], [2.3, 3.4], Math.floor); // returns [2.1]
*
* // Using a property name as an iteratee
* intersectionBy([{ x: 1 }, { x: 2 }], [{ x: 1 }], 'x'); // returns [{ x: 1 }]
*/
export const intersectionBy = <T>(...args: [...arrays: T[][], iteratee: Iteratee<T>]): T[] => {
if (args.length < 2) {
throw new Error("intersectionBy requires at least two arrays and an iteratee");
}
const iteratee = args[args.length - 1];
const arrays = args.slice(0, -1) as T[][];
if (arrays.length === 0) {
return [];
}
const getIterateeValue = (item: T): unknown => {
if (typeof iteratee === "function") {
return (iteratee as (value: T) => any)(item);
} else if (typeof iteratee === "string") {
return (item as any)[iteratee];
} else {
throw new Error("Iteratee must be a function or a string key of the array elements");
}
};
const [first, ...rest] = arrays;
const transformedFirst = first.map((item) => getIterateeValue(item));
const transformedSets: Set<unknown>[] = rest.map(
(array) => new Set(array.map((item) => getIterateeValue(item))),
);
const res: T[] = [];
const seen = new Set<unknown>();
for (let i = 0; i < first.length; i++) {
const item = first[i];
const transformed = transformedFirst[i];
if (seen.has(transformed)) {
continue;
}
const existsInAll = transformedSets.every((set) => set.has(transformed));
if (existsInAll) {
res.push(item);
seen.add(transformed);
}
}
return res;
};

493
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff