diff --git a/code/addons/a11y/package.json b/code/addons/a11y/package.json index f95692592620..e4996d3c3fec 100644 --- a/code/addons/a11y/package.json +++ b/code/addons/a11y/package.json @@ -60,7 +60,7 @@ }, "devDependencies": { "@storybook/global": "^5.0.0", - "@storybook/icons": "^1.2.10", + "@storybook/icons": "^1.2.12", "@testing-library/react": "^14.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/code/addons/backgrounds/package.json b/code/addons/backgrounds/package.json index 21b2df490aec..58fc79dc52c6 100644 --- a/code/addons/backgrounds/package.json +++ b/code/addons/backgrounds/package.json @@ -60,7 +60,7 @@ "ts-dedent": "^2.0.0" }, "devDependencies": { - "@storybook/icons": "^1.2.10", + "@storybook/icons": "^1.2.12", "react": "^18.2.0", "react-dom": "^18.2.0", "typescript": "^5.3.2" diff --git a/code/addons/controls/package.json b/code/addons/controls/package.json index 9b37806994af..0f4e1426c848 100644 --- a/code/addons/controls/package.json +++ b/code/addons/controls/package.json @@ -56,7 +56,7 @@ }, "devDependencies": { "@storybook/blocks": "workspace:*", - "@storybook/icons": "^1.2.10", + "@storybook/icons": "^1.2.12", "react": "^18.2.0", "react-dom": "^18.2.0" }, diff --git a/code/addons/jest/package.json b/code/addons/jest/package.json index a27279e22100..44907e89557b 100644 --- a/code/addons/jest/package.json +++ b/code/addons/jest/package.json @@ -58,7 +58,7 @@ "upath": "^2.0.1" }, "devDependencies": { - "@storybook/icons": "^1.2.10", + "@storybook/icons": "^1.2.12", "react": "^18.2.0", "react-dom": "^18.2.0", "react-resize-detector": "^7.1.2", diff --git a/code/addons/measure/package.json b/code/addons/measure/package.json index 8750536d0b59..5622fb9277ed 100644 --- a/code/addons/measure/package.json +++ b/code/addons/measure/package.json @@ -71,7 +71,7 @@ "tiny-invariant": "^1.3.1" }, "devDependencies": { - "@storybook/icons": "^1.2.10", + "@storybook/icons": "^1.2.12", "react": "^18.2.0", "react-dom": "^18.2.0", "typescript": "^5.3.2" diff --git a/code/addons/onboarding/package.json b/code/addons/onboarding/package.json index 6b644683aa20..c29d39c0b598 100644 --- a/code/addons/onboarding/package.json +++ b/code/addons/onboarding/package.json @@ -49,7 +49,7 @@ }, "devDependencies": { "@radix-ui/react-dialog": "^1.0.5", - "@storybook/icons": "^1.2.10", + "@storybook/icons": "^1.2.12", "@storybook/react": "workspace:*", "framer-motion": "^11.0.3", "react": "^18.2.0", diff --git a/code/addons/outline/package.json b/code/addons/outline/package.json index f072d5a8a685..a800dd81b0c5 100644 --- a/code/addons/outline/package.json +++ b/code/addons/outline/package.json @@ -61,7 +61,7 @@ "ts-dedent": "^2.0.0" }, "devDependencies": { - "@storybook/icons": "^1.2.10", + "@storybook/icons": "^1.2.12", "react": "^18.2.0", "react-dom": "^18.2.0", "typescript": "^5.3.2" diff --git a/code/addons/test/package.json b/code/addons/test/package.json index c2040b6b7526..cb19ee789913 100644 --- a/code/addons/test/package.json +++ b/code/addons/test/package.json @@ -77,7 +77,7 @@ "dependencies": { "@storybook/csf": "^0.1.11", "@storybook/global": "^5.0.0", - "@storybook/icons": "^1.2.10", + "@storybook/icons": "^1.2.12", "@storybook/instrumenter": "workspace:*", "@storybook/test": "workspace:*", "polished": "^4.2.2", @@ -85,7 +85,7 @@ }, "devDependencies": { "@devtools-ds/object-inspector": "^1.1.2", - "@storybook/icons": "^1.2.5", + "@storybook/icons": "^1.2.12", "@types/node": "^22.0.0", "@types/semver": "^7", "@vitest/browser": "^2.1.1", diff --git a/code/addons/test/src/manager.tsx b/code/addons/test/src/manager.tsx index 7d96703be518..4a22540a9508 100644 --- a/code/addons/test/src/manager.tsx +++ b/code/addons/test/src/manager.tsx @@ -33,6 +33,8 @@ addons.register(ADDON_ID, (api) => { icon: , title: 'Component Tests', description: () => 'Not yet run', + runnable: true, + watchable: true, }); addons.add(PANEL_ID, { diff --git a/code/addons/themes/package.json b/code/addons/themes/package.json index ade8a3e6326b..3a5061536b54 100644 --- a/code/addons/themes/package.json +++ b/code/addons/themes/package.json @@ -59,7 +59,7 @@ "ts-dedent": "^2.0.0" }, "devDependencies": { - "@storybook/icons": "^1.2.10", + "@storybook/icons": "^1.2.12", "typescript": "^5.3.2" }, "peerDependencies": { diff --git a/code/addons/viewport/package.json b/code/addons/viewport/package.json index a9f2b5e949f6..b5ba38adce72 100644 --- a/code/addons/viewport/package.json +++ b/code/addons/viewport/package.json @@ -55,7 +55,7 @@ }, "devDependencies": { "@storybook/global": "^5.0.0", - "@storybook/icons": "^1.2.10", + "@storybook/icons": "^1.2.12", "react": "^18.2.0", "react-dom": "^18.2.0", "typescript": "^5.3.2" diff --git a/code/core/package.json b/code/core/package.json index 5aa725236372..0e84e3d9a37a 100644 --- a/code/core/package.json +++ b/code/core/package.json @@ -313,7 +313,7 @@ "@radix-ui/react-slot": "^1.0.2", "@storybook/docs-mdx": "4.0.0-next.1", "@storybook/global": "^5.0.0", - "@storybook/icons": "^1.2.10", + "@storybook/icons": "^1.2.12", "@tanstack/react-virtual": "^3.3.0", "@testing-library/react": "^14.0.0", "@types/compression": "^1.7.0", @@ -360,7 +360,7 @@ "diff": "^5.2.0", "downshift": "^9.0.4", "ejs": "^3.1.10", - "es-toolkit": "^1.21.0", + "es-toolkit": "^1.22.0", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0", "esbuild-plugin-alias": "^0.2.1", "execa": "^8.0.1", diff --git a/code/core/src/components/components/ScrollArea/ScrollArea.tsx b/code/core/src/components/components/ScrollArea/ScrollArea.tsx index aa1dff7b5c24..195f70d68b06 100644 --- a/code/core/src/components/components/ScrollArea/ScrollArea.tsx +++ b/code/core/src/components/components/ScrollArea/ScrollArea.tsx @@ -39,6 +39,7 @@ const ScrollAreaScrollbar = styled(ScrollAreaPrimitive.Scrollbar)<{ background: 'transparent', transition: 'all 0.2s ease-out', borderRadius: 'var(--scrollbar-size)', + zIndex: 1, '&[data-orientation="vertical"]': { width: 'var(--scrollbar-size)', diff --git a/code/core/src/manager/components/layout/Layout.tsx b/code/core/src/manager/components/layout/Layout.tsx index 50e29ba18da1..df9d5b86d9cb 100644 --- a/code/core/src/manager/components/layout/Layout.tsx +++ b/code/core/src/manager/components/layout/Layout.tsx @@ -146,7 +146,6 @@ export const Layout = ({ managerLayoutState, setManagerLayoutState, hasTab, ...s viewMode={managerLayoutState.viewMode} showPanel={showPanel} > - {showPages && {slots.slotPages}} {({ match }) => {slots.slotMain}} @@ -170,7 +169,14 @@ export const Layout = ({ managerLayoutState, setManagerLayoutState, hasTab, ...s )} {isMobile && ( - + <> + + + )} ); diff --git a/code/core/src/manager/components/notifications/NotificationItem.stories.tsx b/code/core/src/manager/components/notifications/NotificationItem.stories.tsx index 1914a0fe7987..71622e8c93a0 100644 --- a/code/core/src/manager/components/notifications/NotificationItem.stories.tsx +++ b/code/core/src/manager/components/notifications/NotificationItem.stories.tsx @@ -23,7 +23,7 @@ const meta = { ), (Story) => ( -
+
), diff --git a/code/core/src/manager/components/notifications/NotificationItem.tsx b/code/core/src/manager/components/notifications/NotificationItem.tsx index 75ae36129b76..159fa0da137f 100644 --- a/code/core/src/manager/components/notifications/NotificationItem.tsx +++ b/code/core/src/manager/components/notifications/NotificationItem.tsx @@ -11,6 +11,8 @@ import { type State } from '@storybook/core/manager-api'; import { transparentize } from 'polished'; +import { MEDIA_DESKTOP_BREAKPOINT } from '../../constants'; + const slideIn = keyframes({ '0%': { opacity: 0, @@ -35,17 +37,21 @@ const Notification = styled.div<{ duration?: number }>( ({ theme }) => ({ position: 'relative', display: 'flex', - padding: 15, - width: 280, - borderRadius: 4, + border: `1px solid ${theme.appBorderColor}`, + padding: '12px 6px 12px 12px', + borderRadius: theme.appBorderRadius + 1, alignItems: 'center', animation: `${slideIn} 500ms`, background: theme.base === 'light' ? 'hsla(203, 50%, 20%, .97)' : 'hsla(203, 30%, 95%, .97)', - boxShadow: `0 2px 5px 0 rgba(0,0,0,0.05), 0 5px 15px 0 rgba(0,0,0,0.1)`, + boxShadow: `0 2px 5px 0 rgba(0, 0, 0, 0.05), 0 5px 15px 0 rgba(0, 0, 0, 0.1)`, color: theme.color.inverseText, textDecoration: 'none', overflow: 'hidden', + + [MEDIA_DESKTOP_BREAKPOINT]: { + boxShadow: `0 1px 2px 0 rgba(0, 0, 0, 0.05), 0px -5px 20px 10px ${theme.background.app}`, + }, }), ({ duration, theme }) => duration && { @@ -107,9 +113,8 @@ const NotificationTextWrapper = styled.div(({ theme }) => ({ const Headline = styled.div<{ hasIcon: boolean }>(({ theme, hasIcon }) => ({ height: '100%', - width: hasIcon ? 205 : 230, alignItems: 'center', - whiteSpace: 'nowrap', + whiteSpace: 'balance', overflow: 'hidden', textOverflow: 'ellipsis', fontSize: theme.typography.size.s1, @@ -122,6 +127,7 @@ const SubHeadline = styled.div(({ theme }) => ({ fontSize: theme.typography.size.s1 - 1, lineHeight: '14px', marginTop: 2, + whiteSpace: 'balance', })); const ItemContent: FC> = ({ @@ -154,6 +160,7 @@ const ItemContent: FC> = ({ }; const DismissButtonWrapper = styled(IconButton)(({ theme }) => ({ + width: 28, alignSelf: 'center', marginTop: 0, color: theme.base === 'light' ? 'rgba(255,255,255,0.7)' : ' #999999', @@ -181,9 +188,11 @@ export const NotificationItemSpacer = styled.div({ const NotificationItem: FC<{ notification: State['notifications'][0]; onDismissNotification: (id: string) => void; + zIndex?: number; }> = ({ notification: { content, duration, link, onClear, onClick, id, icon }, onDismissNotification, + zIndex, }) => { const onTimeout = useCallback(() => { onDismissNotification(id); @@ -191,7 +200,7 @@ const NotificationItem: FC<{ if (onClear) { onClear({ dismissed: false, timeout: true }); } - }, [onDismissNotification, onClear]); + }, [id, onDismissNotification, onClear]); const timer = useRef | null>(null); useEffect(() => { @@ -211,11 +220,11 @@ const NotificationItem: FC<{ if (onClear) { onClear({ dismissed: true, timeout: false }); } - }, [onDismissNotification, onClear]); + }, [id, onDismissNotification, onClear]); if (link) { return ( - + @@ -224,7 +233,11 @@ const NotificationItem: FC<{ if (onClick) { return ( - onClick({ onDismiss })}> + onClick({ onDismiss })} + style={{ zIndex }} + > @@ -232,7 +245,7 @@ const NotificationItem: FC<{ } return ( - + diff --git a/code/core/src/manager/components/notifications/NotificationList.tsx b/code/core/src/manager/components/notifications/NotificationList.tsx index a0c8fabd13c6..0e4789a28bf5 100644 --- a/code/core/src/manager/components/notifications/NotificationList.tsx +++ b/code/core/src/manager/components/notifications/NotificationList.tsx @@ -2,11 +2,10 @@ import type { FC } from 'react'; import React from 'react'; import { styled } from '@storybook/core/theming'; -import type { CSSObject } from '@storybook/core/theming'; import type { State } from '@storybook/core/manager-api'; -import { MEDIA_DESKTOP_BREAKPOINT } from '../../constants'; +import { useLayout } from '../layout/LayoutProvider'; import NotificationItem from './NotificationItem'; interface NotificationListProps { @@ -18,35 +17,36 @@ export const NotificationList: FC = ({ notifications, clearNotification, }) => { + const { isMobile } = useLayout(); return ( - + {notifications && - notifications.map((notification) => ( + notifications.map((notification, index) => ( clearNotification(id)} notification={notification} + zIndex={notifications.length - index} /> ))} ); }; -const List = styled.div<{ placement?: CSSObject }>({ - zIndex: 200, - position: 'fixed', - left: 20, - bottom: 60, - - [MEDIA_DESKTOP_BREAKPOINT]: { - bottom: 20, - }, - - '> * + *': { - marginTop: 10, - }, - - '&:empty': { - display: 'none', +const List = styled.div<{ isMobile?: boolean }>( + { + zIndex: 200, + '> * + *': { + marginTop: 12, + }, + '&:empty': { + display: 'none', + }, }, -}); + ({ isMobile }) => + isMobile && { + position: 'fixed', + bottom: 40, + margin: 20, + } +); diff --git a/code/core/src/manager/components/sidebar/Sidebar.stories.tsx b/code/core/src/manager/components/sidebar/Sidebar.stories.tsx index 30be9d3e9bf5..410403971002 100644 --- a/code/core/src/manager/components/sidebar/Sidebar.stories.tsx +++ b/code/core/src/manager/components/sidebar/Sidebar.stories.tsx @@ -42,6 +42,7 @@ const managerContext: any = { 'api::getShortcutKeys' ), getChannel: fn().mockName('api::getChannel'), + getElements: fn(() => ({})), selectStory: fn().mockName('api::selectStory'), experimental_setFilter: fn().mockName('api::experimental_setFilter'), }, diff --git a/code/core/src/manager/components/sidebar/Sidebar.tsx b/code/core/src/manager/components/sidebar/Sidebar.tsx index f658f00e4bf8..c752e76586d5 100644 --- a/code/core/src/manager/components/sidebar/Sidebar.tsx +++ b/code/core/src/manager/components/sidebar/Sidebar.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useMemo } from 'react'; import { Button, ScrollArea, Spaced } from '@storybook/core/components'; import { styled } from '@storybook/core/theming'; @@ -6,13 +6,12 @@ import type { API_LoadedRefData, Addon_SidebarTopType } from '@storybook/core/ty import { TESTING_MODULE_RUN_ALL_REQUEST, - TESTING_MODULE_WATCH_MODE_REQUEST, type TestingModuleRunAllRequestPayload, - type TestingModuleWatchModeRequestPayload, } from '@storybook/core/core-events'; import { type State, useStorybookApi } from '@storybook/core/manager-api'; import { MEDIA_DESKTOP_BREAKPOINT } from '../../constants'; +import { useLayout } from '../layout/LayoutProvider'; import { Explorer } from './Explorer'; import type { HeadingProps } from './Heading'; import { Heading } from './Heading'; @@ -51,19 +50,6 @@ const Top = styled(Spaced)({ flex: 1, }); -const Bottom = styled.div(({ theme }) => ({ - borderTop: `1px solid ${theme.appBorderColor}`, - padding: theme.layoutMargin / 2, - display: 'flex', - flexWrap: 'wrap', - gap: theme.layoutMargin / 2, - backgroundColor: theme.barBg, - - '&:empty': { - display: 'none', - }, -})); - const Swap = React.memo(function Swap({ children, condition, @@ -140,15 +126,7 @@ export const Sidebar = React.memo(function Sidebar({ const dataset = useCombination(index, indexError, previewInitialized, status, refs); const isLoading = !index && !indexError; const lastViewedProps = useLastViewed(selected); - const api = useStorybookApi(); - const [watchMode, setWatchMode] = useState(false); - - useEffect(() => { - api.emit(TESTING_MODULE_WATCH_MODE_REQUEST, { - providerId: TEST_PROVIDER_ID, - watchMode, - } as TestingModuleWatchModeRequestPayload); - }, [api, watchMode]); + const { isMobile } = useLayout(); return ( @@ -200,25 +178,8 @@ export const Sidebar = React.memo(function Sidebar({ )} + {isMobile || isLoading ? null : } - {isLoading ? null : ( - - - - - - )} ); }); diff --git a/code/core/src/manager/components/sidebar/SidebarBottom.stories.tsx b/code/core/src/manager/components/sidebar/SidebarBottom.stories.tsx index 498750fd82e0..466222b2723c 100644 --- a/code/core/src/manager/components/sidebar/SidebarBottom.stories.tsx +++ b/code/core/src/manager/components/sidebar/SidebarBottom.stories.tsx @@ -1,3 +1,7 @@ +import React from 'react'; + +import { Addon_TypesEnum } from '@storybook/core/types'; +import { ContrastIcon, PointerHandIcon } from '@storybook/icons'; import { fn } from '@storybook/test'; import { SidebarBottomBase } from './SidebarBottom'; @@ -6,8 +10,28 @@ export default { component: SidebarBottomBase, args: { api: { - experimental_setFilter: fn(), + clearNotification: fn(), emit: fn(), + experimental_setFilter: fn(), + getElements: fn(() => ({ + 'component-tests': { + type: Addon_TypesEnum.experimental_TEST_PROVIDER, + id: 'component-tests', + title: 'Component tests', + description: () => 'Ran 2 seconds ago', + icon: , + runnable: true, + watchable: true, + }, + 'visual-tests': { + type: Addon_TypesEnum.experimental_TEST_PROVIDER, + id: 'visual-tests', + title: 'Visual tests', + description: () => 'Not run', + icon: , + runnable: true, + }, + })), }, }, }; diff --git a/code/core/src/manager/components/sidebar/SidebarBottom.tsx b/code/core/src/manager/components/sidebar/SidebarBottom.tsx index 290e44ac8f6a..fb62aead6d4d 100644 --- a/code/core/src/manager/components/sidebar/SidebarBottom.tsx +++ b/code/core/src/manager/components/sidebar/SidebarBottom.tsx @@ -1,11 +1,17 @@ -import React, { useCallback, useEffect } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { styled } from '@storybook/core/theming'; -import type { API_FilterFunction, API_StatusUpdate, API_StatusValue } from '@storybook/types'; +import { + type API_FilterFunction, + type API_StatusUpdate, + type API_StatusValue, + Addon_TypesEnum, +} from '@storybook/core/types'; import { + TESTING_MODULE_RUN_ALL_REQUEST, TESTING_MODULE_RUN_PROGRESS_RESPONSE, - type TestingModuleRunProgressPayload, + TESTING_MODULE_WATCH_MODE_REQUEST, type TestingModuleRunResponsePayload, } from '@storybook/core/core-events'; import { @@ -14,9 +20,11 @@ import { useStorybookApi, useStorybookState, } from '@storybook/core/manager-api'; -import { useChannel } from '@storybook/core/preview-api'; -import { FilterToggle } from './FilterToggle'; +import { throttle } from 'es-toolkit'; + +import { NotificationList } from '../notifications/NotificationList'; +import { TestingModule } from './TestingModule'; const filterNone: API_FilterFunction = () => true; const filterWarn: API_FilterFunction = ({ status = {} }) => @@ -26,30 +34,47 @@ const filterError: API_FilterFunction = ({ status = {} }) => const filterBoth: API_FilterFunction = ({ status = {} }) => Object.values(status).some((value) => value?.status === 'warn' || value?.status === 'error'); -const getFilter = (showWarnings = false, showErrors = false) => { - if (showWarnings && showErrors) { +const getFilter = (warningsActive = false, errorsActive = false) => { + if (warningsActive && errorsActive) { return filterBoth; } - if (showWarnings) { + if (warningsActive) { return filterWarn; } - if (showErrors) { + if (errorsActive) { return filterError; } return filterNone; }; const Wrapper = styled.div({ - display: 'flex', - gap: 5, + transition: 'height 250ms', }); -interface SidebarBottomProps { - api: API; - status: State['status']; -} +const Content = styled.div(({ theme }) => ({ + position: 'absolute', + bottom: 0, + left: 0, + right: 0, + padding: 12, + display: 'flex', + flexDirection: 'column', + gap: 12, + color: theme.color.defaultText, + fontSize: theme.typography.size.s1, + + '&:empty': { + display: 'none', + }, + + // Integrators can use these to style their custom additions + '--sb-sidebar-bottom-card-background': theme.background.content, + '--sb-sidebar-bottom-card-border': `1px solid ${theme.appBorderColor}`, + '--sb-sidebar-bottom-card-border-radius': `${theme.appBorderRadius + 1}px`, + '--sb-sidebar-bottom-card-box-shadow': `0 1px 2px 0 rgba(0, 0, 0, 0.05), 0px -5px 20px 10px ${theme.background.app}`, +})); const statusMap: Record = { failed: 'error', @@ -75,9 +100,30 @@ function processTestReport(payload: TestingModuleRunResponsePayload) { return result; } -export const SidebarBottomBase = ({ api, status = {} }: SidebarBottomProps) => { - const [showWarnings, setShowWarnings] = React.useState(false); - const [showErrors, setShowErrors] = React.useState(false); +interface SidebarBottomProps { + api: API; + notifications: State['notifications']; + status: State['status']; +} + +export const SidebarBottomBase = ({ api, notifications = [], status = {} }: SidebarBottomProps) => { + const [warningsActive, setWarningsActive] = useState(false); + const [errorsActive, setErrorsActive] = useState(false); + const [contentHeight, setContentHeight] = useState(0); + + const resizeObserverCallback = useMemo( + () => throttle((element) => setContentHeight(element.clientHeight || 0), 250), + [] + ); + + useEffect(() => { + const wrapper = document.getElementById('sidebar-bottom'); + if (wrapper) { + const resizeObserver = new ResizeObserver(() => resizeObserverCallback(wrapper)); + resizeObserver.observe(wrapper); + return () => resizeObserver.disconnect(); + } + }, [resizeObserverCallback]); const warnings = Object.values(status).filter((statusByAddonId) => Object.values(statusByAddonId).some((value) => value?.status === 'warn') @@ -88,47 +134,55 @@ export const SidebarBottomBase = ({ api, status = {} }: SidebarBottomProps) => { const hasWarnings = warnings.length > 0; const hasErrors = errors.length > 0; - const toggleWarnings = useCallback(() => setShowWarnings((shown) => !shown), []); - const toggleErrors = useCallback(() => setShowErrors((shown) => !shown), []); + const onRunTests = useCallback( + (providerId?: string) => { + api.emit(TESTING_MODULE_RUN_ALL_REQUEST, { providerId }); + }, + [api] + ); + const onSetWatchMode = useCallback( + (providerId: string, watchMode: boolean) => { + api.emit(TESTING_MODULE_WATCH_MODE_REQUEST, { providerId, watchMode }); + }, + [api] + ); useEffect(() => { - const filter = getFilter(hasWarnings && showWarnings, hasErrors && showErrors); + const filter = getFilter(hasWarnings && warningsActive, hasErrors && errorsActive); api.experimental_setFilter('sidebar-bottom-filter', filter); - }, [api, hasWarnings, hasErrors, showWarnings, showErrors]); + }, [api, hasWarnings, hasErrors, warningsActive, errorsActive]); - if (!hasWarnings && !hasErrors) { + const testProviders = Object.values(api.getElements(Addon_TypesEnum.experimental_TEST_PROVIDER)); + + if (!hasWarnings && !hasErrors && !testProviders.length) { return null; } return ( - - {hasErrors && ( - - )} - {hasWarnings && ( - + + + - )} + ); }; export const SidebarBottom = () => { const api = useStorybookApi(); - const { status } = useStorybookState(); + const { notifications, status } = useStorybookState(); useEffect(() => { api.getChannel()?.on(TESTING_MODULE_RUN_PROGRESS_RESPONSE, (data) => { @@ -142,5 +196,5 @@ export const SidebarBottom = () => { }); }, [api]); - return ; + return ; }; diff --git a/code/core/src/manager/components/sidebar/TestingModule.stories.tsx b/code/core/src/manager/components/sidebar/TestingModule.stories.tsx new file mode 100644 index 000000000000..d3a151f003cf --- /dev/null +++ b/code/core/src/manager/components/sidebar/TestingModule.stories.tsx @@ -0,0 +1,115 @@ +import React from 'react'; + +import { Addon_TypesEnum } from '@storybook/core/types'; +import { ContrastIcon, MarkupIcon, PointerHandIcon } from '@storybook/icons'; +import type { Meta, StoryObj } from '@storybook/react'; +import { fn, userEvent } from '@storybook/test'; + +import { TestingModule } from './TestingModule'; + +const testProviders = [ + { + type: Addon_TypesEnum.experimental_TEST_PROVIDER, + id: 'component-tests', + title: 'Component tests', + description: () => 'Ran 2 seconds ago', + icon: , + runnable: true, + watchable: true, + }, + { + type: Addon_TypesEnum.experimental_TEST_PROVIDER, + id: 'visual-tests', + title: 'Visual tests', + description: () => 'Not run', + icon: , + runnable: true, + }, + { + type: Addon_TypesEnum.experimental_TEST_PROVIDER, + id: 'linting', + title: 'Linting', + description: () => 'Watching for changes', + icon: , + watching: true, + }, +]; + +const meta = { + component: TestingModule, + args: { + testProviders, + errorCount: 0, + errorsActive: false, + setErrorsActive: fn(), + warningCount: 0, + warningsActive: false, + setWarningsActive: fn(), + onRunTests: fn(), + onSetWatchMode: fn(), + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; + +export const Collapsed: Story = { + play: async ({ canvas }) => { + const button = await canvas.findByRole('button', { name: /Collapse/ }); + await userEvent.click(button); + }, +}; + +export const Statuses: Story = { + args: { + errorCount: 14, + warningCount: 42, + }, +}; + +export const ErrorsActive: Story = { + args: { + ...Statuses.args, + errorsActive: true, + }, +}; + +export const WarningsActive: Story = { + args: { + ...Statuses.args, + warningsActive: true, + }, +}; + +export const BothActive: Story = { + args: { + ...Statuses.args, + errorsActive: true, + warningsActive: true, + }, +}; + +export const CollapsedStatuses: Story = { + args: Statuses.args, + play: Collapsed.play, +}; + +export const Running: Story = { + args: { + testProviders: testProviders.map((tp) => ({ ...tp, running: true })), + }, +}; + +export const CollapsedRunning: Story = { + args: Running.args, + play: Collapsed.play, +}; + +export const Watching: Story = { + args: { + testProviders: testProviders.map((tp) => ({ ...tp, watching: true })), + }, +}; diff --git a/code/core/src/manager/components/sidebar/TestingModule.tsx b/code/core/src/manager/components/sidebar/TestingModule.tsx new file mode 100644 index 000000000000..48964c6b5101 --- /dev/null +++ b/code/core/src/manager/components/sidebar/TestingModule.tsx @@ -0,0 +1,320 @@ +import React, { type SyntheticEvent, useCallback, useEffect, useRef, useState } from 'react'; + +import { Button } from '@storybook/core/components'; +import { keyframes, styled } from '@storybook/core/theming'; +import { + ChevronSmallUpIcon, + EyeIcon, + PlayAllHollowIcon, + PlayHollowIcon, + StopAltHollowIcon, +} from '@storybook/icons'; +import type { Addon_TestProviderType } from '@storybook/types'; + +const DEFAULT_HEIGHT = 500; + +const spin = keyframes({ + from: { transform: 'rotate(0deg)' }, + to: { transform: 'rotate(360deg)' }, +}); + +const Outline = styled.div<{ active: boolean }>(({ theme, active }) => ({ + position: 'relative', + lineHeight: '20px', + width: '100%', + padding: 1, + overflow: 'hidden', + background: 'var(--sb-sidebar-bottom-card-background)', + border: 'var(--sb-sidebar-bottom-card-border)', + borderRadius: 'var(--sb-sidebar-bottom-card-border-radius)' as any, + boxShadow: 'var(--sb-sidebar-bottom-card-box-shadow)', + transitionProperty: 'color, background-color, border-color, text-decoration-color, fill, stroke', + transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)', + transitionDuration: '0.15s', + + '&:after': { + content: '""', + display: active ? 'block' : 'none', + position: 'absolute', + left: '50%', + top: '50%', + marginLeft: 'calc(max(100vw, 100vh) * -0.5)', + marginTop: 'calc(max(100vw, 100vh) * -0.5)', + height: 'max(100vw, 100vh)', + width: 'max(100vw, 100vh)', + animation: `${spin} 3s linear infinite`, + background: + 'conic-gradient(rgba(255, 71, 133, 0.2) 0deg, rgb(255, 71, 133) 0deg, transparent 160deg)', + opacity: 1, + willChange: 'auto', + }, +})); + +const Card = styled.div(({ theme }) => ({ + position: 'relative', + zIndex: 1, + borderRadius: theme.appBorderRadius, + backgroundColor: theme.background.content, + + '&:hover #testing-module-collapse-toggle': { + opacity: 1, + }, +})); + +const Collapsible = styled.div(({ theme }) => ({ + overflow: 'hidden', + transition: 'max-height 250ms', + willChange: 'auto', + boxShadow: `inset 0 -1px 0 ${theme.appBorderColor}`, +})); + +const Content = styled.div({ + padding: '12px 6px', + display: 'flex', + flexDirection: 'column', + gap: '12px', +}); + +const Bar = styled.div<{ onClick?: (e: SyntheticEvent) => void }>(({ onClick }) => ({ + display: 'flex', + width: '100%', + cursor: onClick ? 'pointer' : 'default', + userSelect: 'none', + alignItems: 'center', + justifyContent: 'space-between', + overflow: 'hidden', + padding: '6px', +})); + +const Filters = styled.div({ + display: 'flex', + flexBasis: '100%', + justifyContent: 'flex-end', + gap: 6, +}); + +const CollapseToggle = styled(Button)({ + opacity: 0, + transition: 'opacity 250ms', + willChange: 'auto', + '&:focus, &:hover': { + opacity: 1, + }, +}); + +const StatusButton = styled(Button)<{ status: 'negative' | 'warning' }>( + { minWidth: 28 }, + ({ active, status, theme }) => + !active && + (theme.base === 'light' + ? { + background: { + negative: theme.background.negative, + warning: theme.background.warning, + }[status], + color: { + negative: theme.color.negativeText, + warning: theme.color.warningText, + }[status], + } + : { + background: { + negative: `${theme.color.negative}22`, + warning: `${theme.color.warning}22`, + }[status], + color: { + negative: theme.color.negative, + warning: theme.color.warning, + }[status], + }) +); + +const TestProvider = styled.div({ + display: 'flex', + justifyContent: 'space-between', + gap: 6, +}); + +const Info = styled.div({ + display: 'flex', + gap: 6, +}); + +const Actions = styled.div({ + display: 'flex', + gap: 6, +}); + +const Details = styled.div({ + display: 'flex', + flexDirection: 'column', +}); + +const Title = styled.div(({ theme }) => ({ + fontSize: theme.typography.size.s2, +})); + +const Description = styled.div(({ theme }) => ({ + fontSize: theme.typography.size.s1, + color: theme.barTextColor, +})); + +const Icon = styled.div(({ theme }) => ({ + color: theme.barTextColor, + padding: '2px 6px', +})); + +interface TestingModuleProps { + testProviders: (Addon_TestProviderType & { running?: boolean; watching?: boolean })[]; + errorCount: number; + errorsActive: boolean; + setErrorsActive: (active: boolean) => void; + warningCount: number; + warningsActive: boolean; + setWarningsActive: (active: boolean) => void; + onRunTests: (providerId?: string) => void; + onSetWatchMode: (providerId: string, watchMode: boolean) => void; +} + +export const TestingModule = ({ + testProviders, + errorCount, + errorsActive, + setErrorsActive, + warningCount, + warningsActive, + setWarningsActive, + onRunTests, + onSetWatchMode, +}: TestingModuleProps) => { + const contentRef = useRef(null); + const [collapsed, setCollapsed] = useState(false); + const [maxHeight, setMaxHeight] = useState(DEFAULT_HEIGHT); + + useEffect(() => { + setMaxHeight(contentRef.current?.offsetHeight || DEFAULT_HEIGHT); + }, []); + + const toggleCollapsed = () => { + setMaxHeight(contentRef.current?.offsetHeight || DEFAULT_HEIGHT); + setCollapsed(!collapsed); + }; + + const active = testProviders.some((tp) => tp.running); + const testing = testProviders.length > 0; + + return ( + + + + + {testProviders.map( + ({ id, icon, title, description, runnable, running, watchable, watching }) => ( + + + {icon} +
+ {title} + {description({})} +
+
+ + {watchable && ( + + )} + {runnable && ( + + )} + +
+ ) + )} +
+
+ + + {testing && ( + + )} + + {testing && ( + + + + )} + + {errorCount > 0 && ( + { + e.stopPropagation(); + setErrorsActive(!errorsActive); + }} + aria-label="Show errors" + > + {errorCount < 100 ? errorCount : '99+'} + + )} + {warningCount > 0 && ( + { + e.stopPropagation(); + setWarningsActive(!warningsActive); + }} + aria-label="Show warnings" + > + {warningCount < 100 ? warningCount : '99+'} + + )} + + +
+
+ ); +}; diff --git a/code/core/src/manager/globals/exports.ts b/code/core/src/manager/globals/exports.ts index b7cac2e8a3c6..c01fdf8415a9 100644 --- a/code/core/src/manager/globals/exports.ts +++ b/code/core/src/manager/globals/exports.ts @@ -214,7 +214,9 @@ export default { 'PhotoIcon', 'PinAltIcon', 'PinIcon', + 'PlayAllHollowIcon', 'PlayBackIcon', + 'PlayHollowIcon', 'PlayIcon', 'PlayNextIcon', 'PlusIcon', @@ -235,6 +237,7 @@ export default { 'RequestChangeIcon', 'RewindIcon', 'RulerIcon', + 'SaveIcon', 'SearchIcon', 'ShareAltIcon', 'ShareIcon', @@ -252,6 +255,7 @@ export default { 'StatusPassIcon', 'StatusWarnIcon', 'StickerIcon', + 'StopAltHollowIcon', 'StopAltIcon', 'StopIcon', 'StorybookIcon', diff --git a/code/core/src/types/modules/addons.ts b/code/core/src/types/modules/addons.ts index 5aa530064699..8099f88b941e 100644 --- a/code/core/src/types/modules/addons.ts +++ b/code/core/src/types/modules/addons.ts @@ -468,6 +468,8 @@ export interface Addon_TestProviderType { icon: ReactNode; title: string; description: FC; + runnable?: boolean; + watchable?: boolean; } type Addon_TypeBaseNames = Exclude< diff --git a/code/lib/blocks/package.json b/code/lib/blocks/package.json index f14f71319ded..54b5c0e7d421 100644 --- a/code/lib/blocks/package.json +++ b/code/lib/blocks/package.json @@ -45,10 +45,10 @@ "dependencies": { "@storybook/csf": "^0.1.11", "@storybook/global": "^5.0.0", - "@storybook/icons": "^1.2.10", + "@storybook/icons": "^1.2.12", "color-convert": "^2.0.1", "dequal": "^2.0.2", - "es-toolkit": "^1.21.0", + "es-toolkit": "^1.22.0", "markdown-to-jsx": "^7.4.5", "memoizerific": "^1.11.3", "polished": "^4.2.2", diff --git a/code/lib/codemod/package.json b/code/lib/codemod/package.json index 0f3ab191752e..2c6ba312e285 100644 --- a/code/lib/codemod/package.json +++ b/code/lib/codemod/package.json @@ -61,7 +61,7 @@ "@storybook/csf": "^0.1.11", "@types/cross-spawn": "^6.0.2", "cross-spawn": "^7.0.3", - "es-toolkit": "^1.21.0", + "es-toolkit": "^1.22.0", "globby": "^14.0.1", "jscodeshift": "^0.15.1", "prettier": "^3.1.1", diff --git a/code/lib/source-loader/package.json b/code/lib/source-loader/package.json index b21a1a2fec33..d85849b7ea4e 100644 --- a/code/lib/source-loader/package.json +++ b/code/lib/source-loader/package.json @@ -45,7 +45,7 @@ }, "dependencies": { "@storybook/csf": "^0.1.11", - "es-toolkit": "^1.21.0", + "es-toolkit": "^1.22.0", "estraverse": "^5.2.0", "prettier": "^3.1.1" }, diff --git a/code/package.json b/code/package.json index 240dc465751a..8bed0733fa94 100644 --- a/code/package.json +++ b/code/package.json @@ -185,7 +185,7 @@ "create-storybook": "workspace:*", "cross-env": "^7.0.3", "danger": "^12.3.3", - "es-toolkit": "^1.21.0", + "es-toolkit": "^1.22.0", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0", "esbuild-loader": "^4.2.0", "esbuild-plugin-alias": "^0.2.1", diff --git a/code/renderers/react/package.json b/code/renderers/react/package.json index 97ba5f8b0c7c..1a71e6fd4315 100644 --- a/code/renderers/react/package.json +++ b/code/renderers/react/package.json @@ -89,7 +89,7 @@ "@types/semver": "^7.3.4", "@types/util-deprecate": "^1.0.0", "babel-plugin-react-docgen": "^4.2.1", - "es-toolkit": "^1.21.0", + "es-toolkit": "^1.22.0", "expect-type": "^0.15.0", "require-from-string": "^2.0.2" }, diff --git a/code/yarn.lock b/code/yarn.lock index 77bd47b36b2f..ff12b2b6fca1 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -5335,7 +5335,7 @@ __metadata: dependencies: "@storybook/addon-highlight": "workspace:*" "@storybook/global": "npm:^5.0.0" - "@storybook/icons": "npm:^1.2.10" + "@storybook/icons": "npm:^1.2.12" "@testing-library/react": "npm:^14.0.0" axe-core: "npm:^4.2.0" react: "npm:^18.2.0" @@ -5373,7 +5373,7 @@ __metadata: resolution: "@storybook/addon-backgrounds@workspace:addons/backgrounds" dependencies: "@storybook/global": "npm:^5.0.0" - "@storybook/icons": "npm:^1.2.10" + "@storybook/icons": "npm:^1.2.12" memoizerific: "npm:^1.11.3" react: "npm:^18.2.0" react-dom: "npm:^18.2.0" @@ -5390,7 +5390,7 @@ __metadata: dependencies: "@storybook/blocks": "workspace:*" "@storybook/global": "npm:^5.0.0" - "@storybook/icons": "npm:^1.2.10" + "@storybook/icons": "npm:^1.2.12" dequal: "npm:^2.0.2" react: "npm:^18.2.0" react-dom: "npm:^18.2.0" @@ -5506,7 +5506,7 @@ __metadata: resolution: "@storybook/addon-jest@workspace:addons/jest" dependencies: "@storybook/global": "npm:^5.0.0" - "@storybook/icons": "npm:^1.2.10" + "@storybook/icons": "npm:^1.2.12" react: "npm:^18.2.0" react-dom: "npm:^18.2.0" react-resize-detector: "npm:^7.1.2" @@ -5553,7 +5553,7 @@ __metadata: resolution: "@storybook/addon-measure@workspace:addons/measure" dependencies: "@storybook/global": "npm:^5.0.0" - "@storybook/icons": "npm:^1.2.10" + "@storybook/icons": "npm:^1.2.12" react: "npm:^18.2.0" react-dom: "npm:^18.2.0" tiny-invariant: "npm:^1.3.1" @@ -5568,7 +5568,7 @@ __metadata: resolution: "@storybook/addon-onboarding@workspace:addons/onboarding" dependencies: "@radix-ui/react-dialog": "npm:^1.0.5" - "@storybook/icons": "npm:^1.2.10" + "@storybook/icons": "npm:^1.2.12" "@storybook/react": "workspace:*" framer-motion: "npm:^11.0.3" react: "npm:^18.2.0" @@ -5587,7 +5587,7 @@ __metadata: resolution: "@storybook/addon-outline@workspace:addons/outline" dependencies: "@storybook/global": "npm:^5.0.0" - "@storybook/icons": "npm:^1.2.10" + "@storybook/icons": "npm:^1.2.12" react: "npm:^18.2.0" react-dom: "npm:^18.2.0" ts-dedent: "npm:^2.0.0" @@ -5619,7 +5619,7 @@ __metadata: version: 0.0.0-use.local resolution: "@storybook/addon-themes@workspace:addons/themes" dependencies: - "@storybook/icons": "npm:^1.2.10" + "@storybook/icons": "npm:^1.2.12" ts-dedent: "npm:^2.0.0" typescript: "npm:^5.3.2" peerDependencies: @@ -5644,7 +5644,7 @@ __metadata: resolution: "@storybook/addon-viewport@workspace:addons/viewport" dependencies: "@storybook/global": "npm:^5.0.0" - "@storybook/icons": "npm:^1.2.10" + "@storybook/icons": "npm:^1.2.12" memoizerific: "npm:^1.11.3" react: "npm:^18.2.0" react-dom: "npm:^18.2.0" @@ -5751,13 +5751,13 @@ __metadata: "@storybook/addon-actions": "workspace:*" "@storybook/csf": "npm:^0.1.11" "@storybook/global": "npm:^5.0.0" - "@storybook/icons": "npm:^1.2.10" + "@storybook/icons": "npm:^1.2.12" "@storybook/react": "workspace:*" "@storybook/test": "workspace:*" "@types/color-convert": "npm:^2.0.0" color-convert: "npm:^2.0.1" dequal: "npm:^2.0.2" - es-toolkit: "npm:^1.21.0" + es-toolkit: "npm:^1.22.0" markdown-to-jsx: "npm:^7.4.5" memoizerific: "npm:^1.11.3" polished: "npm:^4.2.2" @@ -5930,7 +5930,7 @@ __metadata: ansi-regex: "npm:^6.0.1" camelcase: "npm:^8.0.0" cross-spawn: "npm:^7.0.3" - es-toolkit: "npm:^1.21.0" + es-toolkit: "npm:^1.22.0" globby: "npm:^14.0.1" jscodeshift: "npm:^0.15.1" mdast-util-mdx-jsx: "npm:^3.0.0" @@ -6022,7 +6022,7 @@ __metadata: "@storybook/csf": "npm:^0.1.11" "@storybook/docs-mdx": "npm:4.0.0-next.1" "@storybook/global": "npm:^5.0.0" - "@storybook/icons": "npm:^1.2.10" + "@storybook/icons": "npm:^1.2.12" "@tanstack/react-virtual": "npm:^3.3.0" "@testing-library/react": "npm:^14.0.0" "@types/compression": "npm:^1.7.0" @@ -6072,7 +6072,7 @@ __metadata: diff: "npm:^5.2.0" downshift: "npm:^9.0.4" ejs: "npm:^3.1.10" - es-toolkit: "npm:^1.21.0" + es-toolkit: "npm:^1.22.0" esbuild: "npm:^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0" esbuild-plugin-alias: "npm:^0.2.1" esbuild-register: "npm:^3.5.0" @@ -6237,7 +6237,7 @@ __metadata: "@devtools-ds/object-inspector": "npm:^1.1.2" "@storybook/csf": "npm:^0.1.11" "@storybook/global": "npm:^5.0.0" - "@storybook/icons": "npm:^1.2.5" + "@storybook/icons": "npm:^1.2.12" "@storybook/instrumenter": "workspace:*" "@storybook/test": "workspace:*" "@types/node": "npm:^22.0.0" @@ -6347,7 +6347,17 @@ __metadata: languageName: unknown linkType: soft -"@storybook/icons@npm:^1.2.10, @storybook/icons@npm:^1.2.5": +"@storybook/icons@npm:^1.2.12": + version: 1.2.12 + resolution: "@storybook/icons@npm:1.2.12" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 10c0/97f6a7b7841fb5a0d1c8a30c36173469e7b0814a674c8103c7c0fd8803f0f7c2a778545af864012d40883195a533534dbc98541deac2bafe31e6a3fe37fdfc66 + languageName: node + linkType: hard + +"@storybook/icons@npm:^1.2.5": version: 1.2.10 resolution: "@storybook/icons@npm:1.2.10" peerDependencies: @@ -6777,7 +6787,7 @@ __metadata: acorn-jsx: "npm:^5.3.1" acorn-walk: "npm:^7.2.0" babel-plugin-react-docgen: "npm:^4.2.1" - es-toolkit: "npm:^1.21.0" + es-toolkit: "npm:^1.22.0" escodegen: "npm:^2.1.0" expect-type: "npm:^0.15.0" html-tags: "npm:^3.1.0" @@ -6901,7 +6911,7 @@ __metadata: create-storybook: "workspace:*" cross-env: "npm:^7.0.3" danger: "npm:^12.3.3" - es-toolkit: "npm:^1.21.0" + es-toolkit: "npm:^1.22.0" esbuild: "npm:^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0" esbuild-loader: "npm:^4.2.0" esbuild-plugin-alias: "npm:^0.2.1" @@ -7003,7 +7013,7 @@ __metadata: resolution: "@storybook/source-loader@workspace:lib/source-loader" dependencies: "@storybook/csf": "npm:^0.1.11" - es-toolkit: "npm:^1.21.0" + es-toolkit: "npm:^1.22.0" estraverse: "npm:^5.2.0" prettier: "npm:^3.1.1" typescript: "npm:^5.3.2" @@ -14159,10 +14169,10 @@ __metadata: languageName: node linkType: hard -"es-toolkit@npm:^1.21.0": - version: 1.21.0 - resolution: "es-toolkit@npm:1.21.0" - checksum: 10c0/894a63f8ce5b2e5c1be242c8e8eace6364ea1212d01cdf89594d2cc582c5e1574114ad2ee7022ad5206561c4d5170511d83b38853257249860e56178768854ea +"es-toolkit@npm:^1.22.0": + version: 1.22.0 + resolution: "es-toolkit@npm:1.22.0" + checksum: 10c0/a167789f727437d435071af74e22c0c4a5a557aa61a5013a1656d24b1c8636c88d6b74f12ad0c3966b74d3f56e432d8e8d1989e5c10c10fda8eba5752783af18 languageName: node linkType: hard diff --git a/scripts/package.json b/scripts/package.json index 456ba6c06038..ff66a0feab87 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -107,7 +107,7 @@ "detect-port": "^1.6.1", "ejs": "^3.1.10", "ejs-lint": "^2.0.0", - "es-toolkit": "^1.21.0", + "es-toolkit": "^1.22.0", "esbuild": "^0.23.0", "esbuild-plugin-alias": "^0.2.1", "eslint": "^8.57.0", diff --git a/scripts/yarn.lock b/scripts/yarn.lock index a0e46537c940..d6e69cea50b0 100644 --- a/scripts/yarn.lock +++ b/scripts/yarn.lock @@ -1571,7 +1571,7 @@ __metadata: detect-port: "npm:^1.6.1" ejs: "npm:^3.1.10" ejs-lint: "npm:^2.0.0" - es-toolkit: "npm:^1.21.0" + es-toolkit: "npm:^1.22.0" esbuild: "npm:^0.23.0" esbuild-plugin-alias: "npm:^0.2.1" eslint: "npm:^8.57.0" @@ -5308,10 +5308,10 @@ __metadata: languageName: node linkType: hard -"es-toolkit@npm:^1.21.0": - version: 1.21.0 - resolution: "es-toolkit@npm:1.21.0" - checksum: 10c0/894a63f8ce5b2e5c1be242c8e8eace6364ea1212d01cdf89594d2cc582c5e1574114ad2ee7022ad5206561c4d5170511d83b38853257249860e56178768854ea +"es-toolkit@npm:^1.22.0": + version: 1.22.0 + resolution: "es-toolkit@npm:1.22.0" + checksum: 10c0/a167789f727437d435071af74e22c0c4a5a557aa61a5013a1656d24b1c8636c88d6b74f12ad0c3966b74d3f56e432d8e8d1989e5c10c10fda8eba5752783af18 languageName: node linkType: hard