diff --git a/.changeset/tiny-frogs-tease.md b/.changeset/tiny-frogs-tease.md new file mode 100644 index 000000000..a3b77da92 --- /dev/null +++ b/.changeset/tiny-frogs-tease.md @@ -0,0 +1,5 @@ +--- +'@vapor-ui/composites': minor +--- + +add new `Menu` composites diff --git a/package.json b/package.json index af23b6cc5..3c9d881cd 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "clean": "turbo run clean && rimraf node_modules .turbo", "codemod": "pnpm --filter @vapor-ui/codemod", "color-generator": "pnpm --filter @vapor-ui/color-generator", + "composites": "pnpm --filter @vapor-ui/composites", "core": "pnpm --filter @vapor-ui/core", "css-generator": "pnpm --filter @vapor-ui/css-generator", "dev": "turbo dev", diff --git a/packages/composites/src/components/menu/index.parts.ts b/packages/composites/src/components/menu/index.parts.ts new file mode 100644 index 000000000..d246a93cb --- /dev/null +++ b/packages/composites/src/components/menu/index.parts.ts @@ -0,0 +1,8 @@ +export { + MenuRoot as Root, + MenuGroup as Group, + MenuItem as Item, + MenuCheckGroup as CheckGroup, + MenuCheckItem as CheckItem, + MenuSubmenu as Submenu, +} from './menu'; diff --git a/packages/composites/src/components/menu/index.ts b/packages/composites/src/components/menu/index.ts new file mode 100644 index 000000000..515d1290d --- /dev/null +++ b/packages/composites/src/components/menu/index.ts @@ -0,0 +1 @@ +export * as Menu from './index.parts'; diff --git a/packages/composites/src/components/menu/menu.stories.tsx b/packages/composites/src/components/menu/menu.stories.tsx new file mode 100644 index 000000000..9b87a73dd --- /dev/null +++ b/packages/composites/src/components/menu/menu.stories.tsx @@ -0,0 +1,134 @@ +import { useState } from 'react'; + +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { Button, Text, VStack } from '@vapor-ui/core'; +import { ChevronDoubleRightOutlineIcon, PlusOutlineIcon } from '@vapor-ui/icons'; + +import { Menu } from '.'; + +export default { + title: 'Composites/Menu', + component: Menu.Root, + argTypes: { + defaultOpen: { control: 'boolean' }, + modal: { control: 'boolean' }, + isDisabled: { control: 'boolean' }, + side: { control: 'inline-radio', options: ['top', 'bottom', 'left', 'right'] }, + align: { control: 'inline-radio', options: ['start', 'center', 'end'] }, + }, +} satisfies Meta; + +type Story = StoryObj; + +export const Default: Story = { + render: (args) => { + return ( +
+ Menu Trigger} defaultOpen {...args}> + + alert('clicked item 1')} /> + } + onClick={() => alert('clicked item 2')} + /> + } + trailing={} + onClick={() => alert('clicked item 3')} + /> + } + onClick={() => alert('clicked item 4')} + /> + } + variant="critical" + onClick={() => alert('clicked item 5')} + /> + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ); + }, +}; + +export const Controlled: Story = { + render: (args) => { + const [singleValue, setSingleValue] = useState(); + const [multipleValue, setMultipleValue] = useState(); + + return ( + + 1. Selected Single Value: {singleValue ?? Not Selected} + + 2. Selected Multiple Value:{' '} + {multipleValue ? multipleValue.join(', ') : Not Selected} + + + + Open Menu + + } + {...args} + > + + + + + + + + + + + + + + + + ); + }, +}; diff --git a/packages/composites/src/components/menu/menu.tsx b/packages/composites/src/components/menu/menu.tsx new file mode 100644 index 000000000..b04e1e552 --- /dev/null +++ b/packages/composites/src/components/menu/menu.tsx @@ -0,0 +1,507 @@ +import { createContext, useContext, useMemo } from 'react'; +import type { ReactNode } from 'react'; + +import { useControlled } from '@base-ui/utils/useControlled'; +import { useStableCallback } from '@base-ui/utils/useStableCallback'; +import { Box, Menu as MenuPrimitives } from '@vapor-ui/core'; + +import type { SlotProps } from '~/utils/create-slots'; +import { createSlots } from '~/utils/create-slots'; + +const slots = createSlots({ + trigger: MenuPrimitives.Trigger, +}); + +export const MenuRoot = ({ + // functional + open, + defaultOpen, + onOpenChange, + modal, + actionsRef, + container, + + // variants + isDisabled, + side, + align, + + // slots + trigger, + children, +}: MenuRoot.Props) => { + return ( + + + + + + {children} + + + + ); +}; + +type Slots = SlotProps; +type RootProps = MenuPrimitives.Root.Props; +type PositionerProps = MenuPrimitives.PositionerPrimitive.Props; +type PortalProps = MenuPrimitives.PortalPrimitive.Props; + +export interface MenuRootProps { + /** + * 메뉴 열림 상태(제어). 사용자 액션에 반응한 상태 변경을 외부에서 추적해야 할 때 사용한다. + * 상태의 변경을 추적할 필요가 없다면 defaultOpen을 사용한다. + */ + open?: RootProps['open']; + + /** + * 열림 상태 변경 콜백. 트리거·오버레이 클릭·ESC 등 모든 열림/닫힘 경로에서 호출된다. + * @example + * setOpen(open)} /> + */ + onOpenChange?: RootProps['onOpenChange']; + + /** + * 마운트 시 초기 열림 여부(비제어). + * @default false + */ + defaultOpen?: RootProps['defaultOpen']; + + /** + * 메뉴가 모달로 동작할지 여부. `true`이면 열려 있는 동안 배경 인터랙션이 차단된다. + * @default true + */ + modal?: RootProps['modal']; + + /** + * 메뉴를 프로그래밍적으로 조작하기 위한 ref를 지정한다. + * @example + * const actionsRef = useRef(null); + * actionsRef.current?.unmount(); + */ + actionsRef?: RootProps['actionsRef']; + + /** + * 트리거를 비활성화한다. 비활성화된 트리거는 메뉴를 열 수 없다. + * @default false + */ + isDisabled?: RootProps['disabled']; + + /** + * Portal 대상 컨테이너. SSR·shadow DOM·특정 스택 컨텍스트에서 팝업 위치 제어가 필요할 때만 지정한다. + * @default document.body + */ + container?: PortalProps['container']; + + /** + * 트리거를 기준으로 팝업이 열리는 방향. + * @default "bottom" + */ + side?: PositionerProps['side']; + + /** + * 트리거와 팝업의 정렬 기준. + * @default "center" + */ + align?: PositionerProps['align']; + + /** + * 메뉴를 여는 진입 요소. + * @example + * 메뉴 열기} /> + */ + trigger?: Slots['trigger']; + + /** + * 그룹 내부에 표시할 요소. + */ + children: ReactNode; +} + +export namespace MenuRoot { + export type Actions = MenuPrimitives.Root.Actions; + export type Props = MenuRootProps; +} + +/* -----------------------------------------------------------------------------------------------*/ + +const groupSlots = createSlots({ + label: MenuPrimitives.GroupLabel, +}); + +export const MenuGroup = ({ label, children }: MenuGroup.Props) => { + return ( + + + {children} + + ); +}; + +type GroupSlots = SlotProps; + +export interface MenuGroupProps { + /** + * 그룹 상단에 표시되는 라벨. 스크린리더가 그룹 이름으로 낭독한다. + */ + label: GroupSlots['label']; + + /** + * 그룹 내부에 표시할 요소. + */ + children: ReactNode; +} + +export namespace MenuGroup { + export type Props = MenuGroupProps; +} + +/* -----------------------------------------------------------------------------------------------*/ + +const itemSlots = createSlots({ + leading: Box, + trailing: Box, + label: Box, +}); + +export const MenuItem = ({ variant, label, leading, trailing, onClick }: MenuItem.Props) => { + return ( + + + + + + ); +}; + +type ItemSlots = SlotProps; +type ItemProps = MenuPrimitives.Item.Props; + +export interface MenuItemProps { + /** + * 항목 클릭 시 호출되는 이벤트 핸들러. + * @example + * + */ + onClick: ItemProps['onClick']; + + /** + * 메뉴 아이템의 시각적 위계를 결정한다. + * @default "default" + */ + variant?: 'default' | 'critical'; + + /** + * 항목 좌측에 표시되는 요소. 주로 아이콘을 배치한다. + */ + leading?: ItemSlots['leading']; + + /** + * 항목 우측에 표시되는 요소. 단축키·보조 아이콘 등을 배치한다. + */ + trailing?: ItemSlots['trailing']; + + /** + * 항목의 텍스트 라벨. + */ + label: ItemSlots['label']; +} + +export namespace MenuItem { + export type Props = MenuItemProps; +} + +/* -----------------------------------------------------------------------------------------------*/ + +type MenuCheckGroupContextValue = + | { mode: 'single' } + | { mode: 'multiple'; value: string[]; handleValueChange: (value: string) => void }; + +const MenuCheckGroupContext = createContext(undefined); + +const useMenuCheckGroupContext = () => { + const context = useContext(MenuCheckGroupContext); + if (context === undefined) { + throw new Error( + 'MenuCheckGroupContext is missing. MenuCheckGroup parts must be placed within .', + ); + } + return context; +}; + +/* -----------------------------------------------------------------------------------------------*/ + +const checkGroupSlots = createSlots({ + label: MenuPrimitives.GroupLabel, +}); + +export const MenuCheckGroup = (props: MenuCheckGroup.Props) => { + const { mode } = props; + + if (mode === 'single') return ; + return ; +}; + +const SingleCheckGroup = ({ + value, + defaultValue, + onValueChange, + label, + children, +}: MenuSingleCheckGroupProps) => { + const context = useMemo(() => ({ mode: 'single' }), []); + + return ( + + + + {children} + + + ); +}; + +const MultipleCheckGroup = ({ + value: valueProp, + defaultValue, + onValueChange, + label, + children, +}: MenuMultipleCheckGroupProps) => { + const [value, setValueState] = useControlled({ + name: 'MenuCheckGroup', + controlled: valueProp, + default: defaultValue ?? [], + }); + + const handleValueChange = useStableCallback((nextValue: string) => { + const next = value.includes(nextValue) + ? value.filter((v) => v !== nextValue) + : [...value, nextValue]; + + onValueChange?.(next); + setValueState(next); + }); + + const context = useMemo( + () => ({ mode: 'multiple', value, handleValueChange }), + [value, handleValueChange], + ); + + return ( + + + + {children} + + + ); +}; + +type CheckGroupSlots = SlotProps; + +export interface MenuSingleCheckGroupProps { + /** + * 선택 모드. `single`은 라디오 그룹처럼 최대 하나의 값만 선택된다. + */ + mode: 'single'; + + /** + * 선택된 값(제어). 상태의 변경을 추적할 필요가 없다면 defaultValue를 사용한다. + */ + value?: string; + + /** + * 마운트 시 초기 선택 값(비제어). + */ + defaultValue?: string; + + /** + * 선택이 바뀔 때 호출된다. 새로 선택된 값 하나가 전달된다. + * @example + * setValue(value)} /> + */ + onValueChange?: (value: string) => void; + + /** + * 그룹 상단에 표시되는 라벨. 스크린리더가 그룹 이름으로 낭독한다. + */ + label: CheckGroupSlots['label']; + + /** + * 그룹 내부에 표시할 요소. + */ + children: ReactNode; +} + +export interface MenuMultipleCheckGroupProps { + /** + * 선택 모드. `multiple`은 체크박스 그룹처럼 여러 값을 토글한다. + */ + mode: 'multiple'; + + /** + * 선택된 값 배열(제어). 상태의 변경을 추적할 필요가 없다면 defaultValue를 사용한다. + */ + value?: string[]; + + /** + * 마운트 시 초기 선택 값 배열(비제어). + * @default [] + */ + defaultValue?: string[]; + + /** + * 선택이 바뀔 때 호출된다. 토글 이후의 전체 선택 배열이 전달된다. + * @example + * setValues(values)} /> + */ + onValueChange?: (value: string[]) => void; + + /** + * 그룹 상단에 표시되는 라벨. 스크린리더가 그룹 이름으로 낭독한다. + */ + label: CheckGroupSlots['label']; + + /** + * 그룹 내부에 표시할 요소. + */ + children: ReactNode; +} + +export namespace MenuCheckGroup { + export type Mode = 'single' | 'multiple'; + export type Props = MenuSingleCheckGroupProps | MenuMultipleCheckGroupProps; +} + +/* -----------------------------------------------------------------------------------------------*/ + +const checkItemSlots = createSlots({ + label: Box, + trailing: Box, +}); + +export const MenuCheckItem = ({ + value: valueProp, + onClick, + label, + trailing, +}: MenuCheckItem.Props) => { + const context = useMenuCheckGroupContext(); + const { mode } = context; + + if (mode === 'single') { + return ( + + + + + ); + } + + const { value, handleValueChange } = context; + + return ( + handleValueChange(valueProp)} + onClick={onClick} + > + + + + ); +}; + +type CheckItemSlots = SlotProps; +type RadioItemProps = MenuPrimitives.RadioItemPrimitive.Props; +type CheckboxItemProps = MenuPrimitives.CheckboxItemPrimitive.Props; + +export interface MenuCheckItemProps { + /** + * 항목이 대표하는 값. 그룹의 선택 상태 키로 사용된다. + */ + value: string; + + /** + * 항목 클릭 시 호출된다. 선택 토글은 그룹이 처리하므로, 추가 사이드이펙트가 필요한 경우에만 지정한다. + * @example + * + */ + onClick?: RadioItemProps['onClick'] | CheckboxItemProps['onClick']; + + /** + * 항목의 텍스트 라벨. + */ + label: CheckItemSlots['label']; + + /** + * 항목 우측에 표시되는 요소. 단축키·보조 아이콘 등을 배치한다. + */ + trailing?: CheckItemSlots['trailing']; +} + +export namespace MenuCheckItem { + export type Props = MenuCheckItemProps; +} + +/* -----------------------------------------------------------------------------------------------*/ + +const submenuSlots = createSlots({ + trigger: MenuPrimitives.SubmenuTriggerItem, +}); + +export const MenuSubmenu = ({ trigger, children }: MenuSubmenu.Props) => { + return ( + + + + + + + {children} + + + + + ); +}; + +type SubmenuSlots = SlotProps; + +export interface MenuSubmenuProps extends Omit { + /** + * 중첩 메뉴를 여는 아이템의 레이블. React Element를 전달하여 커스텀할 수도 있다. + * @example + * // #1 + * + * // #2 + * } />} /> + */ + trigger: SubmenuSlots['trigger']; + + /** + * 중첩 메뉴 내부에 표시할 요소. + */ + children: ReactNode; +} + +export namespace MenuSubmenu { + export type Props = MenuSubmenuProps; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8ba58d114..7bffffb41 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -434,13 +434,13 @@ importers: version: 9.39.5(jiti@2.7.0) jest: specifier: ^30.4.2 - version: 30.4.2(@types/node@20.19.43)(esbuild-register@3.6.0(esbuild@0.28.2))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)) + version: 30.4.2(@types/node@20.19.43)(esbuild-register@3.6.0(esbuild@0.27.7))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)) rimraf: specifier: ^6.1.3 version: 6.1.3 ts-jest: specifier: ^29.4.12 - version: 29.4.12(@babel/core@7.29.0)(@jest/transform@30.4.1)(@jest/types@30.4.1)(babel-jest@30.4.1(@babel/core@7.29.0))(esbuild@0.28.2)(jest-util@30.4.1)(jest@30.4.2(@types/node@20.19.43)(esbuild-register@3.6.0(esbuild@0.28.2))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.12(@babel/core@7.29.0)(@jest/transform@30.4.1)(@jest/types@30.4.1)(babel-jest@30.4.1(@babel/core@7.29.0))(esbuild@0.27.7)(jest-util@30.4.1)(jest@30.4.2(@types/node@20.19.43)(esbuild-register@3.6.0(esbuild@0.27.7))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)))(typescript@5.9.3) tsup: specifier: ^8.5.1 version: 8.5.1(jiti@2.7.0)(postcss@8.5.26)(tsx@4.23.13)(typescript@5.9.3) @@ -12333,7 +12333,7 @@ snapshots: jest-util: 30.4.1 slash: 3.0.0 - '@jest/core@30.4.2(esbuild-register@3.6.0(esbuild@0.28.2))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3))': + '@jest/core@30.4.2(esbuild-register@3.6.0(esbuild@0.27.7))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3))': dependencies: '@jest/console': 30.4.1 '@jest/pattern': 30.4.0 @@ -12349,7 +12349,7 @@ snapshots: fast-json-stable-stringify: 2.1.0 graceful-fs: 4.2.11 jest-changed-files: 30.4.1 - jest-config: 30.4.2(@types/node@22.20.1)(esbuild-register@3.6.0(esbuild@0.28.2))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)) + jest-config: 30.4.2(@types/node@22.20.1)(esbuild-register@3.6.0(esbuild@0.27.7))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)) jest-haste-map: 30.4.1 jest-message-util: 30.4.1 jest-regex-util: 30.4.0 @@ -15766,10 +15766,10 @@ snapshots: transitivePeerDependencies: - supports-color - esbuild-register@3.6.0(esbuild@0.28.2): + esbuild-register@3.6.0(esbuild@0.27.7): dependencies: debug: 4.4.3 - esbuild: 0.28.2 + esbuild: 0.27.7 transitivePeerDependencies: - supports-color optional: true @@ -17098,15 +17098,15 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@30.4.2(@types/node@20.19.43)(esbuild-register@3.6.0(esbuild@0.28.2))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)): + jest-cli@30.4.2(@types/node@20.19.43)(esbuild-register@3.6.0(esbuild@0.27.7))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)): dependencies: - '@jest/core': 30.4.2(esbuild-register@3.6.0(esbuild@0.28.2))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)) + '@jest/core': 30.4.2(esbuild-register@3.6.0(esbuild@0.27.7))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)) '@jest/test-result': 30.4.1 '@jest/types': 30.4.1 chalk: 4.1.2 exit-x: 0.2.2 import-local: 3.2.0 - jest-config: 30.4.2(@types/node@20.19.43)(esbuild-register@3.6.0(esbuild@0.28.2))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)) + jest-config: 30.4.2(@types/node@20.19.43)(esbuild-register@3.6.0(esbuild@0.27.7))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)) jest-util: 30.4.1 jest-validate: 30.4.1 yargs: 17.7.2 @@ -17117,7 +17117,7 @@ snapshots: - supports-color - ts-node - jest-config@30.4.2(@types/node@20.19.43)(esbuild-register@3.6.0(esbuild@0.28.2))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)): + jest-config@30.4.2(@types/node@20.19.43)(esbuild-register@3.6.0(esbuild@0.27.7))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)): dependencies: '@babel/core': 7.29.0 '@jest/get-type': 30.1.0 @@ -17144,13 +17144,13 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 20.19.43 - esbuild-register: 3.6.0(esbuild@0.28.2) + esbuild-register: 3.6.0(esbuild@0.27.7) ts-node: 10.9.2(@types/node@20.19.43)(typescript@5.9.3) transitivePeerDependencies: - babel-plugin-macros - supports-color - jest-config@30.4.2(@types/node@22.20.1)(esbuild-register@3.6.0(esbuild@0.28.2))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)): + jest-config@30.4.2(@types/node@22.20.1)(esbuild-register@3.6.0(esbuild@0.27.7))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)): dependencies: '@babel/core': 7.29.0 '@jest/get-type': 30.1.0 @@ -17177,7 +17177,7 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 22.20.1 - esbuild-register: 3.6.0(esbuild@0.28.2) + esbuild-register: 3.6.0(esbuild@0.27.7) ts-node: 10.9.2(@types/node@20.19.43)(typescript@5.9.3) transitivePeerDependencies: - babel-plugin-macros @@ -17436,12 +17436,12 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@30.4.2(@types/node@20.19.43)(esbuild-register@3.6.0(esbuild@0.28.2))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)): + jest@30.4.2(@types/node@20.19.43)(esbuild-register@3.6.0(esbuild@0.27.7))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)): dependencies: - '@jest/core': 30.4.2(esbuild-register@3.6.0(esbuild@0.28.2))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)) + '@jest/core': 30.4.2(esbuild-register@3.6.0(esbuild@0.27.7))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)) '@jest/types': 30.4.1 import-local: 3.2.0 - jest-cli: 30.4.2(@types/node@20.19.43)(esbuild-register@3.6.0(esbuild@0.28.2))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)) + jest-cli: 30.4.2(@types/node@20.19.43)(esbuild-register@3.6.0(esbuild@0.27.7))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -19796,12 +19796,12 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-jest@29.4.12(@babel/core@7.29.0)(@jest/transform@30.4.1)(@jest/types@30.4.1)(babel-jest@30.4.1(@babel/core@7.29.0))(esbuild@0.28.2)(jest-util@30.4.1)(jest@30.4.2(@types/node@20.19.43)(esbuild-register@3.6.0(esbuild@0.28.2))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)))(typescript@5.9.3): + ts-jest@29.4.12(@babel/core@7.29.0)(@jest/transform@30.4.1)(@jest/types@30.4.1)(babel-jest@30.4.1(@babel/core@7.29.0))(esbuild@0.27.7)(jest-util@30.4.1)(jest@30.4.2(@types/node@20.19.43)(esbuild-register@3.6.0(esbuild@0.27.7))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)))(typescript@5.9.3): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 handlebars: 4.7.9 - jest: 30.4.2(@types/node@20.19.43)(esbuild-register@3.6.0(esbuild@0.28.2))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)) + jest: 30.4.2(@types/node@20.19.43)(esbuild-register@3.6.0(esbuild@0.27.7))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)) json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 @@ -19814,7 +19814,7 @@ snapshots: '@jest/transform': 30.4.1 '@jest/types': 30.4.1 babel-jest: 30.4.1(@babel/core@7.29.0) - esbuild: 0.28.2 + esbuild: 0.27.7 jest-util: 30.4.1 ts-morph@22.0.0: