diff --git a/code/addons/a11y/package.json b/code/addons/a11y/package.json index 5d164861a0fa..888ec36c89ec 100644 --- a/code/addons/a11y/package.json +++ b/code/addons/a11y/package.json @@ -61,7 +61,7 @@ }, "devDependencies": { "@storybook/global": "^5.0.0", - "@storybook/icons": "^1.2.5", + "@storybook/icons": "^1.2.10", "@testing-library/react": "^14.0.0", "lodash": "^4.17.21", "react": "^18.2.0", diff --git a/code/addons/backgrounds/package.json b/code/addons/backgrounds/package.json index 4b1a210dab04..aebcc5ff419d 100644 --- a/code/addons/backgrounds/package.json +++ b/code/addons/backgrounds/package.json @@ -61,7 +61,7 @@ "ts-dedent": "^2.0.0" }, "devDependencies": { - "@storybook/icons": "^1.2.5", + "@storybook/icons": "^1.2.10", "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 8f4ad3d9b6c8..125442a11683 100644 --- a/code/addons/controls/package.json +++ b/code/addons/controls/package.json @@ -57,7 +57,7 @@ }, "devDependencies": { "@storybook/blocks": "workspace:*", - "@storybook/icons": "^1.2.5", + "@storybook/icons": "^1.2.10", "react": "^18.2.0", "react-dom": "^18.2.0" }, diff --git a/code/addons/interactions/package.json b/code/addons/interactions/package.json index 70c63b2d806e..7229161ea6df 100644 --- a/code/addons/interactions/package.json +++ b/code/addons/interactions/package.json @@ -61,7 +61,7 @@ }, "devDependencies": { "@devtools-ds/object-inspector": "^1.1.2", - "@storybook/icons": "^1.2.5", + "@storybook/icons": "^1.2.10", "@types/node": "^18.0.0", "formik": "^2.2.9", "react": "^18.2.0", diff --git a/code/addons/jest/package.json b/code/addons/jest/package.json index 119c37ef97f5..34ee5f5282f9 100644 --- a/code/addons/jest/package.json +++ b/code/addons/jest/package.json @@ -59,7 +59,7 @@ "upath": "^2.0.1" }, "devDependencies": { - "@storybook/icons": "^1.2.5", + "@storybook/icons": "^1.2.10", "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 a9106dffc019..cf5850783849 100644 --- a/code/addons/measure/package.json +++ b/code/addons/measure/package.json @@ -72,7 +72,7 @@ "tiny-invariant": "^1.3.1" }, "devDependencies": { - "@storybook/icons": "^1.2.5", + "@storybook/icons": "^1.2.10", "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 0c995838e4d6..7343d2d6b578 100644 --- a/code/addons/onboarding/package.json +++ b/code/addons/onboarding/package.json @@ -50,7 +50,7 @@ }, "devDependencies": { "@radix-ui/react-dialog": "^1.0.5", - "@storybook/icons": "^1.2.5", + "@storybook/icons": "^1.2.10", "@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 5dcbc6667e7c..cfc3bdd7f2f6 100644 --- a/code/addons/outline/package.json +++ b/code/addons/outline/package.json @@ -62,7 +62,7 @@ "ts-dedent": "^2.0.0" }, "devDependencies": { - "@storybook/icons": "^1.2.5", + "@storybook/icons": "^1.2.10", "react": "^18.2.0", "react-dom": "^18.2.0", "typescript": "^5.3.2" diff --git a/code/addons/themes/package.json b/code/addons/themes/package.json index acec0b81da19..4ffdc4265285 100644 --- a/code/addons/themes/package.json +++ b/code/addons/themes/package.json @@ -60,7 +60,7 @@ "ts-dedent": "^2.0.0" }, "devDependencies": { - "@storybook/icons": "^1.2.5", + "@storybook/icons": "^1.2.10", "typescript": "^5.3.2" }, "peerDependencies": { diff --git a/code/addons/viewport/package.json b/code/addons/viewport/package.json index d108727bce1f..ed86badc2a76 100644 --- a/code/addons/viewport/package.json +++ b/code/addons/viewport/package.json @@ -56,7 +56,7 @@ }, "devDependencies": { "@storybook/global": "^5.0.0", - "@storybook/icons": "^1.2.5", + "@storybook/icons": "^1.2.10", "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 e1a53367825d..72f998716d45 100644 --- a/code/core/package.json +++ b/code/core/package.json @@ -287,7 +287,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.5", + "@storybook/icons": "^1.2.10", "@tanstack/react-virtual": "^3.3.0", "@testing-library/react": "^14.0.0", "@types/compression": "^1.7.0", diff --git a/code/core/src/components/components/tooltip/ListItem.tsx b/code/core/src/components/components/tooltip/ListItem.tsx index 58528169f165..8b7b9f9b13c4 100644 --- a/code/core/src/components/components/tooltip/ListItem.tsx +++ b/code/core/src/components/components/tooltip/ListItem.tsx @@ -1,4 +1,4 @@ -import type { ReactNode, ComponentProps } from 'react'; +import type { ReactNode, ComponentProps, SyntheticEvent } from 'react'; import React from 'react'; import { styled } from '@storybook/core/theming'; import memoize from 'memoizerific'; @@ -113,15 +113,19 @@ const Left = styled.span( export interface ItemProps { disabled?: boolean; + href?: string; + onClick?: (event: SyntheticEvent, ...args: any[]) => any; } -const Item = styled.a( +const Item = styled.div( ({ theme }) => ({ + width: '100%', + border: 'none', + background: 'none', fontSize: theme.typography.size.s1, transition: 'all 150ms ease-out', color: theme.color.dark, textDecoration: 'none', - cursor: 'pointer', justifyContent: 'space-between', lineHeight: '18px', @@ -132,43 +136,34 @@ const Item = styled.a( '& > * + *': { paddingLeft: 10, }, - - '&:hover': { - background: theme.background.hoverable, - }, - '&:hover svg': { - opacity: 1, - }, }), - ({ disabled }) => - disabled - ? { - cursor: 'not-allowed', - } - : {} + ({ theme, href, onClick }) => + (href || onClick) && { + cursor: 'pointer', + '&:hover': { + background: theme.background.hoverable, + }, + '&:hover svg': { + opacity: 1, + }, + }, + ({ disabled }) => disabled && { cursor: 'not-allowed' } ); -const getItemProps = memoize(100)((onClick, href, LinkWrapper) => { - const result = {}; - - if (onClick) { - Object.assign(result, { - onClick, - }); - } - if (href) { - Object.assign(result, { - href, - }); - } - if (LinkWrapper && href) { - Object.assign(result, { - to: href, +const getItemProps = memoize(100)((onClick, href, LinkWrapper) => ({ + ...(onClick && { + as: 'button', + onClick, + }), + ...(href && { + as: 'a', + href, + ...(LinkWrapper && { as: LinkWrapper, - }); - } - return result; -}); + to: href, + }), + }), +})); export type LinkWrapperType = (props: any) => ReactNode; @@ -200,23 +195,25 @@ const ListItem = ({ LinkWrapper = undefined, ...rest }: ListItemProps) => { - const itemProps = getItemProps(onClick, href, LinkWrapper); const commonProps = { active, disabled }; + const itemProps = getItemProps(onClick, href, LinkWrapper); return ( - - {icon && {icon}} - {title || center ? ( -
- {title && ( - - {title} - - )} - {center && {center}} -
- ) : null} - {right && {right}} + + <> + {icon && {icon}} + {title || center ? ( +
+ {title && ( + + {title} + + )} + {center && {center}} +
+ ) : null} + {right && {right}} +
); }; diff --git a/code/core/src/components/components/tooltip/TooltipLinkList.stories.tsx b/code/core/src/components/components/tooltip/TooltipLinkList.stories.tsx index 84186de35444..8b3bd9798115 100644 --- a/code/core/src/components/components/tooltip/TooltipLinkList.stories.tsx +++ b/code/core/src/components/components/tooltip/TooltipLinkList.stories.tsx @@ -1,5 +1,4 @@ -import type { FunctionComponent, MouseEvent, PropsWithChildren, ReactElement } from 'react'; -import React, { Children, cloneElement } from 'react'; +import React from 'react'; import { action } from '@storybook/addon-actions'; import type { Meta, StoryObj } from '@storybook/react'; import { LinkIcon, LinuxIcon } from '@storybook/icons'; @@ -9,26 +8,6 @@ import ellipseUrl from './assets/ellipse.png'; const onLinkClick = action('onLinkClick'); -interface StoryLinkWrapperProps { - href: string; - passHref?: boolean; -} - -const StoryLinkWrapper: FunctionComponent> = ({ - href, - passHref = false, - children, -}) => { - const child = Children.only(children) as ReactElement; - return cloneElement(child, { - href: passHref && href, - onClick: (e: MouseEvent) => { - e.preventDefault(); - onLinkClick(href); - }, - }); -}; - export default { component: TooltipLinkList, decorators: [ @@ -57,15 +36,16 @@ export const WithoutIcons = { title: 'Link 1', center: 'This is an addition description', href: 'http://google.com', + onClick: onLinkClick, }, { id: '2', title: 'Link 2', center: 'This is an addition description', href: 'http://google.com', + onClick: onLinkClick, }, ], - LinkWrapper: StoryLinkWrapper, }, } satisfies Story; @@ -78,15 +58,16 @@ export const WithOneIcon = { center: 'This is an addition description', icon: , href: 'http://google.com', + onClick: onLinkClick, }, { id: '2', title: 'Link 2', center: 'This is an addition description', href: 'http://google.com', + onClick: onLinkClick, }, ], - LinkWrapper: StoryLinkWrapper, }, } satisfies Story; @@ -99,15 +80,16 @@ export const ActiveWithoutAnyIcons = { active: true, center: 'This is an addition description', href: 'http://google.com', + onClick: onLinkClick, }, { id: '2', title: 'Link 2', center: 'This is an addition description', href: 'http://google.com', + onClick: onLinkClick, }, ], - LinkWrapper: StoryLinkWrapper, }, } satisfies Story; @@ -120,6 +102,7 @@ export const ActiveWithSeparateIcon = { icon: , center: 'This is an addition description', href: 'http://google.com', + onClick: onLinkClick, }, { id: '2', @@ -127,9 +110,9 @@ export const ActiveWithSeparateIcon = { active: true, center: 'This is an addition description', href: 'http://google.com', + onClick: onLinkClick, }, ], - LinkWrapper: StoryLinkWrapper, }, } satisfies Story; @@ -143,15 +126,16 @@ export const ActiveAndIcon = { icon: , center: 'This is an addition description', href: 'http://google.com', + onClick: onLinkClick, }, { id: '2', title: 'Link 2', center: 'This is an addition description', href: 'http://google.com', + onClick: onLinkClick, }, ], - LinkWrapper: StoryLinkWrapper, }, } satisfies Story; @@ -166,6 +150,7 @@ export const WithIllustration = { right: ellipse, center: 'This is an addition description', href: 'http://google.com', + onClick: onLinkClick, }, { id: '2', @@ -173,9 +158,9 @@ export const WithIllustration = { center: 'This is an addition description', right: ellipse, href: 'http://google.com', + onClick: onLinkClick, }, ], - LinkWrapper: StoryLinkWrapper, }, } satisfies Story; @@ -190,6 +175,7 @@ export const WithCustomIcon = { right: ellipse, center: 'This is an addition description', href: 'http://google.com', + onClick: onLinkClick, }, { id: '2', @@ -197,8 +183,8 @@ export const WithCustomIcon = { center: 'This is an addition description', right: ellipse, href: 'http://google.com', + onClick: onLinkClick, }, ], - LinkWrapper: StoryLinkWrapper, }, } satisfies Story; diff --git a/code/core/src/components/components/tooltip/TooltipLinkList.tsx b/code/core/src/components/components/tooltip/TooltipLinkList.tsx index 6f05bf2ec90d..507ad388ee01 100644 --- a/code/core/src/components/components/tooltip/TooltipLinkList.tsx +++ b/code/core/src/components/components/tooltip/TooltipLinkList.tsx @@ -1,4 +1,4 @@ -import type { SyntheticEvent } from 'react'; +import type { ComponentProps, SyntheticEvent } from 'react'; import React, { useCallback } from 'react'; import { styled } from '@storybook/core/theming'; @@ -19,54 +19,38 @@ const List = styled.div( export interface Link extends Omit { id: string; - isGatsby?: boolean; - onClick?: (event: SyntheticEvent, item: ListItemProps) => void; + onClick?: ( + event: SyntheticEvent, + item: Pick + ) => void; } interface ItemProps extends Link { isIndented?: boolean; } -const Item = (props: ItemProps) => { - const { LinkWrapper, onClick: onClickFromProps, id, isIndented, ...rest } = props; - const { title, href, active } = rest; - const onClick = useCallback( - (event: SyntheticEvent) => { - // @ts-expect-error (non strict) - onClickFromProps(event, rest); - }, - [onClickFromProps] - ); - - const hasOnClick = !!onClickFromProps; +const Item = ({ id, onClick, ...rest }: ItemProps) => { + const { active, disabled, title } = rest; - return ( - + const handleClick = useCallback( + (event: SyntheticEvent) => onClick?.(event, { id, active, disabled, title }), + [onClick, id, active, disabled, title] ); + + return ; }; -export interface TooltipLinkListProps { +export interface TooltipLinkListProps extends ComponentProps { links: Link[]; LinkWrapper?: LinkWrapperType; } -// @ts-expect-error (non strict) -export const TooltipLinkList = ({ links, LinkWrapper = null }: TooltipLinkListProps) => { - const hasIcon = links.some((link) => link.icon); +export const TooltipLinkList = ({ links, LinkWrapper, ...props }: TooltipLinkListProps) => { + const isIndented = links.some((link) => link.icon); return ( - - {links.map(({ isGatsby, ...p }) => ( - // @ts-expect-error (non strict) - + + {links.map((link) => ( + ))} ); diff --git a/code/core/src/manager/components/sidebar/IconSymbols.tsx b/code/core/src/manager/components/sidebar/IconSymbols.tsx index 8dec99d6e248..b1b1dd9cc54d 100644 --- a/code/core/src/manager/components/sidebar/IconSymbols.tsx +++ b/code/core/src/manager/components/sidebar/IconSymbols.tsx @@ -18,6 +18,10 @@ const GROUP_ID = 'icon--group'; const COMPONENT_ID = 'icon--component'; const DOCUMENT_ID = 'icon--document'; const STORY_ID = 'icon--story'; +const SUCCESS_ID = 'icon--success'; +const ERROR_ID = 'icon--error'; +const WARNING_ID = 'icon--warning'; +const DOT_ID = 'icon--dot'; export const IconSymbols: FC = () => { return ( @@ -62,14 +66,47 @@ export const IconSymbols: FC = () => { fill="currentColor" /> + + + + + + + + + + + + ); }; -export const UseSymbol: FC<{ type: 'group' | 'component' | 'document' | 'story' }> = ({ type }) => { +export const UseSymbol: FC<{ + type: 'group' | 'component' | 'document' | 'story' | 'success' | 'error' | 'warning' | 'dot'; +}> = ({ type }) => { if (type === 'group') return ; if (type === 'component') return ; if (type === 'document') return ; if (type === 'story') return ; + if (type === 'success') return ; + if (type === 'error') return ; + if (type === 'warning') return ; + if (type === 'dot') return ; return null; }; diff --git a/code/core/src/manager/components/sidebar/SearchResults.tsx b/code/core/src/manager/components/sidebar/SearchResults.tsx index 0259bd2a92e9..9c20f9fe2090 100644 --- a/code/core/src/manager/components/sidebar/SearchResults.tsx +++ b/code/core/src/manager/components/sidebar/SearchResults.tsx @@ -16,6 +16,7 @@ import { matchesKeyCode, matchesModifiers } from '../../keybinding'; import { statusMapping } from '../../utils/status'; import { UseSymbol } from './IconSymbols'; +import { StatusLabel } from './StatusButton'; const { document } = global; @@ -31,6 +32,7 @@ const ResultRow = styled.li<{ isHighlighted: boolean }>(({ theme, isHighlighted cursor: 'pointer', display: 'flex', alignItems: 'start', + justifyContent: 'space-between', textAlign: 'left', color: 'inherit', fontSize: `${theme.typography.size.s2}px`, @@ -54,6 +56,7 @@ const IconWrapper = styled.div({ }); const ResultRowContent = styled.div({ + flex: 1, display: 'flex', flexDirection: 'column', }); @@ -181,7 +184,7 @@ const Result: FC< const nameMatch = matches.find((match: Match) => match.key === 'name'); const pathMatches = matches.filter((match: Match) => match.key === 'path'); - const [i] = item.status ? statusMapping[item.status] : []; + const [icon] = item.status ? statusMapping[item.status] : []; return ( @@ -216,7 +219,7 @@ const Result: FC< ))} - {item.status ? i : null} + {item.status ? {icon} : null} ); }); diff --git a/code/core/src/manager/components/sidebar/StatusButton.tsx b/code/core/src/manager/components/sidebar/StatusButton.tsx new file mode 100644 index 000000000000..7c5008757b11 --- /dev/null +++ b/code/core/src/manager/components/sidebar/StatusButton.tsx @@ -0,0 +1,63 @@ +import type { Theme } from '@emotion/react'; +import { IconButton } from '@storybook/core/components'; +import { styled } from '@storybook/core/theming'; +import type { API_StatusValue } from '@storybook/types'; +import { transparentize } from 'polished'; + +const withStatusColor = ({ theme, status }: { theme: Theme; status: API_StatusValue }) => { + const defaultColor = + theme.base === 'light' + ? transparentize(0.3, theme.color.defaultText) + : transparentize(0.6, theme.color.defaultText); + + return { + color: { + pending: defaultColor, + success: theme.color.positive, + error: theme.color.negative, + warn: theme.color.warning, + unknown: defaultColor, + }[status], + }; +}; + +export const StatusLabel = styled.div<{ status: API_StatusValue }>(withStatusColor, { + margin: 3, +}); + +export const StatusButton = styled(IconButton)<{ + height?: number; + width?: number; + status: API_StatusValue; + selectedItem?: boolean; +}>( + withStatusColor, + ({ theme, height, width }) => ({ + transition: 'none', + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + width: width || 28, + height: height || 28, + + '&:hover': { + color: theme.color.secondary, + }, + + '&:focus': { + color: theme.color.secondary, + borderColor: theme.color.secondary, + + '&:not(:focus-visible)': { + borderColor: 'transparent', + }, + }, + }), + ({ theme, selectedItem }) => + selectedItem && { + '&:hover': { + boxShadow: `inset 0 0 0 2px ${theme.color.secondary}`, + background: 'rgba(255, 255, 255, 0.2)', + }, + } +); diff --git a/code/core/src/manager/components/sidebar/StatusContext.tsx b/code/core/src/manager/components/sidebar/StatusContext.tsx new file mode 100644 index 000000000000..7d0aad92c603 --- /dev/null +++ b/code/core/src/manager/components/sidebar/StatusContext.tsx @@ -0,0 +1,38 @@ +import type { API_StatusObject, API_StatusState, API_StatusValue, StoryId } from '@storybook/types'; +import { createContext, useContext } from 'react'; +import type { ComponentEntry, GroupEntry, StoriesHash } from '../../../manager-api'; +import { getDescendantIds } from '../../utils/tree'; + +export const StatusContext = createContext<{ + data?: StoriesHash; + status?: API_StatusState; + groupStatus?: Record; +}>({}); + +export const useStatusSummary = (item: GroupEntry | ComponentEntry) => { + const { data, status, groupStatus } = useContext(StatusContext); + const summary: { + counts: Record; + statuses: Record>; + } = { + counts: { pending: 0, success: 0, error: 0, warn: 0, unknown: 0 }, + statuses: { pending: {}, success: {}, error: {}, warn: {}, unknown: {} }, + }; + + if ( + data && + status && + groupStatus && + ['pending', 'warn', 'error'].includes(groupStatus[item.id]) + ) { + for (const storyId of getDescendantIds(data, item.id, false)) { + for (const value of Object.values(status[storyId] || {})) { + summary.counts[value.status]++; + summary.statuses[value.status][storyId] = summary.statuses[value.status][storyId] || []; + summary.statuses[value.status][storyId].push(value); + } + } + } + + return summary; +}; diff --git a/code/core/src/manager/components/sidebar/Tree.tsx b/code/core/src/manager/components/sidebar/Tree.tsx index f9adf1e16334..2f26407f847b 100644 --- a/code/core/src/manager/components/sidebar/Tree.tsx +++ b/code/core/src/manager/components/sidebar/Tree.tsx @@ -7,14 +7,23 @@ import type { State, API, } from '@storybook/core/manager-api'; -import { styled } from '@storybook/core/theming'; +import { styled, useTheme } from '@storybook/core/theming'; import { Button, IconButton, TooltipLinkList, WithTooltip } from '@storybook/core/components'; import { transparentize } from 'polished'; -import type { MutableRefObject } from 'react'; +import type { ComponentProps, MutableRefObject } from 'react'; import React, { useCallback, useMemo, useRef } from 'react'; import { PRELOAD_ENTRIES } from '@storybook/core/core-events'; -import { ExpandAltIcon, CollapseIcon as CollapseIconSvg } from '@storybook/icons'; +import { + ExpandAltIcon, + CollapseIcon as CollapseIconSvg, + SyncIcon, + StatusFailIcon, + StatusWarnIcon, + StatusPassIcon, +} from '@storybook/icons'; +import type { StoryId, API_StatusValue } from '@storybook/types'; + import { ComponentNode, DocumentNode, GroupNode, RootNode, StoryNode } from './TreeNode'; import type { ExpandAction, ExpandedState } from './useExpanded'; @@ -31,57 +40,16 @@ import { } from '../../utils/tree'; import { statusMapping, getHighestStatus, getGroupStatus } from '../../utils/status'; import { useLayout } from '../layout/LayoutProvider'; -import { IconSymbols } from './IconSymbols'; +import { IconSymbols, UseSymbol } from './IconSymbols'; import { CollapseIcon } from './components/CollapseIcon'; +import { StatusContext, useStatusSummary } from './StatusContext'; +import { StatusButton } from './StatusButton'; const Container = styled.div<{ hasOrphans: boolean }>((props) => ({ marginTop: props.hasOrphans ? 20 : 0, marginBottom: 20, })); -export const Action = styled.button<{ height?: number; width?: number }>( - ({ theme, height, width }) => ({ - display: 'inline-flex', - alignItems: 'center', - justifyContent: 'center', - width: width || 20, - height: height || 20, - boxSizing: 'border-box', - margin: 0, - marginLeft: 'auto', - padding: 0, - outline: 0, - lineHeight: 'normal', - background: 'none', - border: `1px solid transparent`, - borderRadius: '100%', - cursor: 'pointer', - transition: 'all 150ms ease-out', - color: - theme.base === 'light' - ? transparentize(0.3, theme.color.defaultText) - : transparentize(0.6, theme.color.defaultText), - - '&:hover': { - color: theme.color.secondary, - }, - - '&:focus': { - color: theme.color.secondary, - borderColor: theme.color.secondary, - - '&:not(:focus-visible)': { - borderColor: 'transparent', - }, - }, - - svg: { - width: 10, - height: 10, - }, - }) -); - const CollapseButton = styled.button(({ theme }) => ({ all: 'unset', display: 'flex', @@ -104,15 +72,14 @@ export const LeafNodeStyleWrapper = styled.div(({ theme }) => ({ display: 'flex', justifyContent: 'space-between', alignItems: 'center', - paddingRight: 20, color: theme.color.defaultText, background: 'transparent', minHeight: 28, borderRadius: 4, '&:hover, &:focus': { - outline: 'none', background: transparentize(0.93, theme.color.secondary), + outline: 'none', }, '&[data-selected="true"]': { @@ -120,7 +87,7 @@ export const LeafNodeStyleWrapper = styled.div(({ theme }) => ({ background: theme.color.secondary, fontWeight: theme.typography.weight.bold, - '&:hover, &:focus': { + '&&:hover, &&:focus': { background: theme.color.secondary, }, svg: { color: theme.color.lightest }, @@ -165,19 +132,20 @@ interface NodeProps { setFullyExpanded?: () => void; onSelectStoryId: (itemId: string) => void; status: State['status'][keyof State['status']]; + groupStatus: Record; api: API; } const Node = React.memo(function Node({ item, status, + groupStatus, refId, docsMode, isOrphan, isDisplayed, isSelected, isFullyExpanded, - color, setFullyExpanded, isExpanded, setExpanded, @@ -185,6 +153,7 @@ const Node = React.memo(function Node({ api, }) { const { isDesktop, isMobile, setMobileMenuOpen } = useLayout(); + const theme = useTheme(); if (!isDisplayed) { return null; @@ -197,20 +166,22 @@ const Node = React.memo(function Node({ const statusValue = getHighestStatus(Object.values(status || {}).map((s) => s.status)); const [icon, textColor] = statusMapping[statusValue]; + const statusOrder: API_StatusValue[] = ['success', 'error', 'warn', 'pending', 'unknown']; + return ( (function Node({ )} {icon ? ( event.stopPropagation()} + placement="bottom" tooltip={() => ( ({ - id: k, - title: v.title, - description: v.description, - right: statusMapping[v.status][0], - }))} + links={Object.entries(status || {}) + .sort( + (a, b) => statusOrder.indexOf(a[1].status) - statusOrder.indexOf(b[1].status) + ) + .map(([addonId, value]) => ({ + id: addonId, + title: value.title, + description: value.description, + icon: { + success: , + error: , + warn: , + pending: , + unknown: null, + }[value.status], + onClick: () => { + onSelectStoryId(item.id); + value.onClick?.(); + }, + }))} /> )} - closeOnOutsideClick > - + {icon} - + ) : null} @@ -296,41 +281,96 @@ const Node = React.memo(function Node({ } if (item.type === 'component' || item.type === 'group') { + const { counts, statuses } = useStatusSummary(item); + + const itemStatus = groupStatus?.[item.id]; + const color = itemStatus ? statusMapping[itemStatus][1] : null; const BranchNode = item.type === 'component' ? ComponentNode : GroupNode; + + const createLinks: (onHide: () => void) => ComponentProps['links'] = ( + onHide + ) => { + const links = []; + if (counts.error) { + links.push({ + id: 'errors', + icon: , + title: `${counts.error} ${counts.error === 1 ? 'story' : 'stories'} with errors`, + onClick: () => { + const [firstStoryId, [firstError]] = Object.entries(statuses.error)[0]; + onSelectStoryId(firstStoryId); + firstError.onClick?.(); + onHide(); + }, + }); + } + if (counts.warn) { + links.push({ + id: 'warnings', + icon: , + title: `${counts.warn} ${counts.warn === 1 ? 'story' : 'stories'} with warnings`, + onClick: () => { + const [firstStoryId, [firstWarning]] = Object.entries(statuses.warn)[0]; + onSelectStoryId(firstStoryId); + firstWarning.onClick?.(); + onHide(); + }, + }); + } + return links; + }; + return ( - 0} - isExpanded={isExpanded} - onClick={(event) => { - event.preventDefault(); - setExpanded({ ids: [item.id], value: !isExpanded }); - if (item.type === 'component' && !isExpanded && isDesktop) onSelectStoryId(item.id); - }} - onMouseEnter={() => { - if (item.type === 'component') { - api.emit(PRELOAD_ENTRIES, { - ids: [item.children[0]], - options: { target: refId }, - }); - } - }} > - {(item.renderLabel as (i: typeof item, api: API) => React.ReactNode)?.(item, api) || - item.name} - + 0} + isExpanded={isExpanded} + onClick={(event) => { + event.preventDefault(); + setExpanded({ ids: [item.id], value: !isExpanded }); + if (item.type === 'component' && !isExpanded && isDesktop) onSelectStoryId(item.id); + }} + onMouseEnter={() => { + if (item.type === 'component') { + api.emit(PRELOAD_ENTRIES, { + ids: [item.children[0]], + options: { target: refId }, + }); + } + }} + > + {(item.renderLabel as (i: typeof item, api: API) => React.ReactNode)?.(item, api) || + item.name} + + {['error', 'warn'].includes(itemStatus) && ( + event.stopPropagation()} + placement="bottom" + tooltip={({ onHide }) => } + > + + + + + + + )} + ); } @@ -514,7 +554,6 @@ export const Tree = React.memo<{ } const isDisplayed = !item.parent || ancestry[itemId].every((a: string) => expanded[a]); - const color = groupStatus[itemId] ? statusMapping[groupStatus[itemId]][1] : null; return ( itemId === oid || itemId.startsWith(`${oid}-`))} isDisplayed={isDisplayed} @@ -554,9 +591,11 @@ export const Tree = React.memo<{ status, ]); return ( - 0}> - - {treeItems} - + + 0}> + + {treeItems} + + ); }); diff --git a/code/core/src/manager/components/sidebar/TreeNode.tsx b/code/core/src/manager/components/sidebar/TreeNode.tsx index 7c42a42a8f3d..57c8c7197735 100644 --- a/code/core/src/manager/components/sidebar/TreeNode.tsx +++ b/code/core/src/manager/components/sidebar/TreeNode.tsx @@ -43,14 +43,10 @@ const BranchNode = styled.button<{ gap: 6, paddingTop: 5, paddingBottom: 4, - - '&:hover, &:focus': { - background: transparentize(0.93, theme.color.secondary), - outline: 'none', - }, })); const LeafNode = styled.a<{ depth?: number }>(({ theme, depth = 0 }) => ({ + width: '100%', cursor: 'pointer', color: 'inherit', display: 'flex', diff --git a/code/core/src/manager/container/Menu.stories.tsx b/code/core/src/manager/container/Menu.stories.tsx index 401a39e7e545..05dbd45ae173 100644 --- a/code/core/src/manager/container/Menu.stories.tsx +++ b/code/core/src/manager/container/Menu.stories.tsx @@ -1,5 +1,4 @@ -import type { FC, MouseEvent, PropsWithChildren, ReactElement } from 'react'; -import React, { Children, cloneElement } from 'react'; +import React from 'react'; import { action } from '@storybook/addon-actions'; import type { Meta, StoryObj } from '@storybook/react'; import { TooltipLinkList, WithTooltip } from '@storybook/core/components'; @@ -7,26 +6,6 @@ import { Shortcut } from './Menu'; const onLinkClick = action('onLinkClick'); -interface StoryLinkWrapperProps { - href: string; - passHref?: boolean; -} - -const StoryLinkWrapper: FC> = ({ - href, - passHref = false, - children, -}) => { - const child = Children.only(children) as ReactElement; - return cloneElement(child, { - href: passHref && href, - onClick: (e: MouseEvent) => { - e.preventDefault(); - onLinkClick(href); - }, - }); -}; - export default { component: TooltipLinkList, decorators: [ @@ -56,6 +35,7 @@ export const WithShortcuts = { center: 'This is an addition description', right: , href: 'http://google.com', + onClick: onLinkClick, }, { id: '2', @@ -63,9 +43,9 @@ export const WithShortcuts = { center: 'This is an addition description', right: , href: 'http://google.com', + onClick: onLinkClick, }, ], - LinkWrapper: StoryLinkWrapper, }, } satisfies Story; @@ -79,6 +59,7 @@ export const WithShortcutsActive = { active: true, right: , href: 'http://google.com', + onClick: onLinkClick, }, { id: '2', @@ -86,8 +67,8 @@ export const WithShortcutsActive = { center: 'This is an addition description', right: , href: 'http://google.com', + onClick: onLinkClick, }, ], - LinkWrapper: StoryLinkWrapper, }, } satisfies Story; diff --git a/code/core/src/manager/globals/exports.ts b/code/core/src/manager/globals/exports.ts index 74ac509e61e5..2596f0f01602 100644 --- a/code/core/src/manager/globals/exports.ts +++ b/code/core/src/manager/globals/exports.ts @@ -65,6 +65,8 @@ export default { 'AlignLeftIcon', 'AlignRightIcon', 'AppleIcon', + 'ArrowBottomLeftIcon', + 'ArrowBottomRightIcon', 'ArrowDownIcon', 'ArrowLeftIcon', 'ArrowRightIcon', @@ -72,6 +74,8 @@ export default { 'ArrowSolidLeftIcon', 'ArrowSolidRightIcon', 'ArrowSolidUpIcon', + 'ArrowTopLeftIcon', + 'ArrowTopRightIcon', 'ArrowUpIcon', 'AzureDevOpsIcon', 'BackIcon', @@ -244,6 +248,9 @@ export default { 'StackedIcon', 'StarHollowIcon', 'StarIcon', + 'StatusFailIcon', + 'StatusPassIcon', + 'StatusWarnIcon', 'StickerIcon', 'StopAltIcon', 'StopIcon', @@ -279,6 +286,7 @@ export default { 'WatchIcon', 'WindowsIcon', 'WrenchIcon', + 'XIcon', 'YoutubeIcon', 'ZoomIcon', 'ZoomOutIcon', diff --git a/code/core/src/manager/utils/status.tsx b/code/core/src/manager/utils/status.tsx index 05092844bdc3..f5bad62e0486 100644 --- a/code/core/src/manager/utils/status.tsx +++ b/code/core/src/manager/utils/status.tsx @@ -6,6 +6,7 @@ import { styled } from '@storybook/core/theming'; import { getDescendantIds } from './tree'; import { CircleIcon } from '@storybook/icons'; +import { UseSymbol } from '../components/sidebar/IconSymbols'; const SmallIcons = styled(CircleIcon)({ // specificity hack @@ -25,9 +26,24 @@ export const statusPriority: API_StatusValue[] = ['unknown', 'pending', 'success export const statusMapping: Record = { unknown: [null, null], pending: [, 'currentColor'], - success: [, 'currentColor'], - warn: [, '#A15C20'], - error: [, 'brown'], + success: [ + + + , + 'currentColor', + ], + warn: [ + + + , + '#A15C20', + ], + error: [ + + + , + 'brown', + ], }; export const getHighestStatus = (statuses: API_StatusValue[]): API_StatusValue => { diff --git a/code/core/src/types/modules/api-stories.ts b/code/core/src/types/modules/api-stories.ts index 90a6962c7d1c..7981612ed921 100644 --- a/code/core/src/types/modules/api-stories.ts +++ b/code/core/src/types/modules/api-stories.ts @@ -125,6 +125,7 @@ export interface API_StatusObject { title: string; description: string; data?: any; + onClick?: () => void; } export type API_StatusState = Record>; diff --git a/code/lib/blocks/package.json b/code/lib/blocks/package.json index 0acb8450e13b..4524b5e740fe 100644 --- a/code/lib/blocks/package.json +++ b/code/lib/blocks/package.json @@ -46,7 +46,7 @@ "dependencies": { "@storybook/csf": "^0.1.11", "@storybook/global": "^5.0.0", - "@storybook/icons": "^1.2.5", + "@storybook/icons": "^1.2.10", "@types/lodash": "^4.14.167", "color-convert": "^2.0.1", "dequal": "^2.0.2", diff --git a/code/yarn.lock b/code/yarn.lock index 1b831f85c140..ee0d4014dbc7 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -5097,7 +5097,7 @@ __metadata: dependencies: "@storybook/addon-highlight": "workspace:*" "@storybook/global": "npm:^5.0.0" - "@storybook/icons": "npm:^1.2.5" + "@storybook/icons": "npm:^1.2.10" "@testing-library/react": "npm:^14.0.0" axe-core: "npm:^4.2.0" lodash: "npm:^4.17.21" @@ -5136,7 +5136,7 @@ __metadata: resolution: "@storybook/addon-backgrounds@workspace:addons/backgrounds" dependencies: "@storybook/global": "npm:^5.0.0" - "@storybook/icons": "npm:^1.2.5" + "@storybook/icons": "npm:^1.2.10" memoizerific: "npm:^1.11.3" react: "npm:^18.2.0" react-dom: "npm:^18.2.0" @@ -5152,7 +5152,7 @@ __metadata: resolution: "@storybook/addon-controls@workspace:addons/controls" dependencies: "@storybook/blocks": "workspace:*" - "@storybook/icons": "npm:^1.2.5" + "@storybook/icons": "npm:^1.2.10" dequal: "npm:^2.0.2" lodash: "npm:^4.17.21" react: "npm:^18.2.0" @@ -5250,7 +5250,7 @@ __metadata: dependencies: "@devtools-ds/object-inspector": "npm:^1.1.2" "@storybook/global": "npm:^5.0.0" - "@storybook/icons": "npm:^1.2.5" + "@storybook/icons": "npm:^1.2.10" "@storybook/instrumenter": "workspace:*" "@storybook/test": "workspace:*" "@types/node": "npm:^18.0.0" @@ -5270,7 +5270,7 @@ __metadata: resolution: "@storybook/addon-jest@workspace:addons/jest" dependencies: "@storybook/global": "npm:^5.0.0" - "@storybook/icons": "npm:^1.2.5" + "@storybook/icons": "npm:^1.2.10" react: "npm:^18.2.0" react-dom: "npm:^18.2.0" react-resize-detector: "npm:^7.1.2" @@ -5318,7 +5318,7 @@ __metadata: resolution: "@storybook/addon-measure@workspace:addons/measure" dependencies: "@storybook/global": "npm:^5.0.0" - "@storybook/icons": "npm:^1.2.5" + "@storybook/icons": "npm:^1.2.10" react: "npm:^18.2.0" react-dom: "npm:^18.2.0" tiny-invariant: "npm:^1.3.1" @@ -5333,7 +5333,7 @@ __metadata: resolution: "@storybook/addon-onboarding@workspace:addons/onboarding" dependencies: "@radix-ui/react-dialog": "npm:^1.0.5" - "@storybook/icons": "npm:^1.2.5" + "@storybook/icons": "npm:^1.2.10" "@storybook/react": "workspace:*" framer-motion: "npm:^11.0.3" react: "npm:^18.2.0" @@ -5352,7 +5352,7 @@ __metadata: resolution: "@storybook/addon-outline@workspace:addons/outline" dependencies: "@storybook/global": "npm:^5.0.0" - "@storybook/icons": "npm:^1.2.5" + "@storybook/icons": "npm:^1.2.10" react: "npm:^18.2.0" react-dom: "npm:^18.2.0" ts-dedent: "npm:^2.0.0" @@ -5384,7 +5384,7 @@ __metadata: version: 0.0.0-use.local resolution: "@storybook/addon-themes@workspace:addons/themes" dependencies: - "@storybook/icons": "npm:^1.2.5" + "@storybook/icons": "npm:^1.2.10" ts-dedent: "npm:^2.0.0" typescript: "npm:^5.3.2" peerDependencies: @@ -5409,7 +5409,7 @@ __metadata: resolution: "@storybook/addon-viewport@workspace:addons/viewport" dependencies: "@storybook/global": "npm:^5.0.0" - "@storybook/icons": "npm:^1.2.5" + "@storybook/icons": "npm:^1.2.10" memoizerific: "npm:^1.11.3" react: "npm:^18.2.0" react-dom: "npm:^18.2.0" @@ -5516,7 +5516,7 @@ __metadata: "@storybook/addon-actions": "workspace:*" "@storybook/csf": "npm:^0.1.11" "@storybook/global": "npm:^5.0.0" - "@storybook/icons": "npm:^1.2.5" + "@storybook/icons": "npm:^1.2.10" "@storybook/react": "workspace:*" "@storybook/test": "workspace:*" "@types/color-convert": "npm:^2.0.0" @@ -5764,7 +5764,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.5" + "@storybook/icons": "npm:^1.2.10" "@tanstack/react-virtual": "npm:^3.3.0" "@testing-library/react": "npm:^14.0.0" "@types/compression": "npm:^1.7.0" @@ -6016,13 +6016,13 @@ __metadata: languageName: unknown linkType: soft -"@storybook/icons@npm:^1.2.5": - version: 1.2.5 - resolution: "@storybook/icons@npm:1.2.5" +"@storybook/icons@npm:^1.2.10": + version: 1.2.10 + resolution: "@storybook/icons@npm:1.2.10" peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: 10c0/089622af6de4ab82624d894fbe43688a0eb72f15e6bb8fc19c54fb9f9d7312ce7caf34acebcbd63319dbaef129d8547bc23a5600955d04f6034355e7d82dcfa1 + checksum: 10c0/aadde2efd5c471b78096f29a6393db111ee95174cab94ade0d2859d476262f080aa8ffb414f82932afd81d5c57bed813193a04e92086962bde2224774dac9060 languageName: node linkType: hard