diff --git a/code/core/src/components/components/Badge/Badge.tsx b/code/core/src/components/components/Badge/Badge.tsx index 038af02e8897..b6ffe656dded 100644 --- a/code/core/src/components/components/Badge/Badge.tsx +++ b/code/core/src/components/components/Badge/Badge.tsx @@ -31,45 +31,37 @@ const BadgeWrapper = styled.div( switch (status) { case 'critical': { return { - color: theme.color.critical, - background: theme.background.critical, + color: theme.fgColor.critical, + background: theme.bgColor.critical, + boxShadow: `inset 0 0 0 1px ${theme.borderColor.critical}`, }; } case 'negative': { return { - color: theme.color.negativeText, - background: theme.background.negative, - boxShadow: - theme.base === 'light' - ? `inset 0 0 0 1px ${transparentize(0.9, theme.color.negativeText)}` - : 'none', + color: theme.fgColor.negative, + background: theme.bgColor.negative, + boxShadow: `inset 0 0 0 1px ${theme.borderColor.negative}`, }; } case 'warning': { return { - color: theme.color.warningText, - background: theme.background.warning, - boxShadow: - theme.base === 'light' - ? `inset 0 0 0 1px ${transparentize(0.9, theme.color.warningText)}` - : 'none', + color: theme.fgColor.warning, + background: theme.bgColor.warning, + boxShadow: `inset 0 0 0 1px ${theme.borderColor.warning}`, }; } case 'neutral': { return { - color: theme.textMutedColor, - background: theme.base === 'light' ? theme.background.app : theme.barBg, + color: theme.fgColor.muted, + background: theme.base === 'dark' ? theme.barBg : theme.background.app, boxShadow: `inset 0 0 0 1px ${transparentize(0.8, theme.textMutedColor)}`, }; } case 'positive': { return { - color: theme.color.positiveText, - background: theme.background.positive, - boxShadow: - theme.base === 'light' - ? `inset 0 0 0 1px ${transparentize(0.9, theme.color.positiveText)}` - : 'none', + color: theme.fgColor.positive, + background: theme.bgColor.positive, + boxShadow: `inset 0 0 0 1px ${theme.borderColor.positive}`, }; } case 'active': { diff --git a/code/core/src/manager/components/sidebar/SearchResults.tsx b/code/core/src/manager/components/sidebar/SearchResults.tsx index f88398606d69..f8e06afa6119 100644 --- a/code/core/src/manager/components/sidebar/SearchResults.tsx +++ b/code/core/src/manager/components/sidebar/SearchResults.tsx @@ -10,10 +10,10 @@ import { TrashIcon } from '@storybook/icons'; import type { ControllerStateAndHelpers } from 'downshift'; import { transparentize } from 'polished'; import { useStorybookApi } from 'storybook/manager-api'; -import { styled } from 'storybook/theming'; +import { styled, useTheme } from 'storybook/theming'; import { matchesKeyCode, matchesModifiers } from '../../keybinding'; -import { statusMapping } from '../../utils/status'; +import { getStatus } from '../../utils/status'; import { UseSymbol } from './IconSymbols'; import { NoResults } from './NoResults'; import { StatusLabel } from './StatusButton'; @@ -159,6 +159,7 @@ const Result: FC< isHighlighted: boolean; } & React.DetailedHTMLProps, HTMLLIElement> > = React.memo(function Result({ item, matches, onClick, ...props }) { + const theme = useTheme(); const click: MouseEventHandler = useCallback( (event) => { event.preventDefault(); @@ -172,12 +173,12 @@ const Result: FC< if (api && props.isHighlighted && item.type === 'component') { api.emit(PRELOAD_ENTRIES, { ids: [item.children[0]] }, { options: { target: item.refId } }); } - }, [props.isHighlighted, item]); + }, [api, props.isHighlighted, item]); const nameMatch = matches.find((match: Match) => match.key === 'name'); const pathMatches = matches.filter((match: Match) => match.key === 'path'); - const [icon] = item.status ? statusMapping[item.status] : []; + const [icon] = item.status ? getStatus(theme, item.status) : []; return ( diff --git a/code/core/src/manager/components/sidebar/Tree.tsx b/code/core/src/manager/components/sidebar/Tree.tsx index 997e58455d31..df743f996dd6 100644 --- a/code/core/src/manager/components/sidebar/Tree.tsx +++ b/code/core/src/manager/components/sidebar/Tree.tsx @@ -34,7 +34,7 @@ import { styled, useTheme } from 'storybook/theming'; import type { Link } from '../../../components/components/tooltip/TooltipLinkList'; import { MEDIA_DESKTOP_BREAKPOINT } from '../../constants'; -import { getGroupStatus, getMostCriticalStatusValue, statusMapping } from '../../utils/status'; +import { getGroupStatus, getMostCriticalStatusValue, getStatus } from '../../utils/status'; import { createId, getAncestorIds, @@ -219,6 +219,7 @@ const Node = React.memo(function Node(props) { onSelectStoryId, api, } = props; + const theme = useTheme(); const { isDesktop, isMobile, setMobileMenuOpen } = useLayout(); const { counts, statusesByValue } = useStatusSummary(item); @@ -287,6 +288,71 @@ const Node = React.memo(function Node(props) { ? useContextMenu(item, statusLinks, api) : { node: null, onMouseEnter: () => {} }; + if ( + (item.type === 'story' && + !('children' in item && item.children) && + (!('subtype' in item) || item.subtype !== 'test')) || + item.type === 'docs' + ) { + const LeafNode = item.type === 'docs' ? DocumentNode : StoryNode; + + const statusValue = getMostCriticalStatusValue( + Object.values(statuses || {}).map((s) => s.value) + ); + const [icon, textColor] = getStatus(theme, statusValue); + + return ( + + { + event.preventDefault(); + onSelectStoryId(item.id); + + if (isMobile) { + setMobileMenuOpen(false); + } + }} + {...(item.type === 'docs' && { docsMode })} + > + {(item.renderLabel as (i: typeof item, api: API) => React.ReactNode)?.(item, api) || + item.name} + + {isSelected && ( + + Skip to canvas + + )} + {contextMenu.node} + {icon ? ( + + {icon} + + ) : null} + + ); + } + if (item.type === 'root') { return ( (function Node(props) { } const itemStatus = getMostCriticalStatusValue(Object.values(statuses || {}).map((s) => s.value)); - const [itemIcon, itemColor] = statusMapping[itemStatus]; + const [itemIcon, itemColor] = getStatus(theme, itemStatus); const itemStatusButton = itemIcon ? ( (function Node(props) { const { children = [] } = item; const BranchNode = { component: ComponentNode, group: GroupNode, story: StoryNode }[item.type]; const status = getMostCriticalStatusValue([itemStatus, groupStatus?.[item.id]]); - const color = status ? statusMapping[status][1] : null; + const color = status ? getStatus(theme, status)[1] : null; const showBranchStatus = status === 'status-value:error' || status === 'status-value:warning'; return ( @@ -459,7 +525,6 @@ const Node = React.memo(function Node(props) { setMobileMenuOpen(false); } }} - {...(item.type === 'docs' && { docsMode })} > {(item.renderLabel as (i: typeof item, api: API) => React.ReactNode)?.(item, api) || item.name} diff --git a/code/core/src/manager/globals/exports.ts b/code/core/src/manager/globals/exports.ts index 5a193d06cd24..c33359fc6338 100644 --- a/code/core/src/manager/globals/exports.ts +++ b/code/core/src/manager/globals/exports.ts @@ -379,6 +379,7 @@ export default { 'lighten', 'styled', 'themes', + 'tokens', 'typography', 'useTheme', 'withTheme', diff --git a/code/core/src/manager/utils/status.tsx b/code/core/src/manager/utils/status.tsx index 9d545d7a9701..984e85e4c85e 100644 --- a/code/core/src/manager/utils/status.tsx +++ b/code/core/src/manager/utils/status.tsx @@ -6,7 +6,7 @@ import { type API_HashEntry, type StatusesByStoryIdAndTypeId } from 'storybook/i import { CircleIcon } from '@storybook/icons'; -import { styled } from 'storybook/theming'; +import { type Theme, styled } from 'storybook/theming'; import { UseSymbol } from '../components/sidebar/IconSymbols'; import { getDescendantIds } from './tree'; @@ -31,27 +31,31 @@ export const statusPriority: StatusValue[] = [ 'status-value:warning', 'status-value:error', ]; -export const statusMapping: Record = { - ['status-value:unknown']: [null, null], - ['status-value:pending']: [, 'currentColor'], - ['status-value:success']: [ - - - , - 'currentColor', - ], - ['status-value:warning']: [ - - - , - '#A15C20', - ], - ['status-value:error']: [ - - - , - '#D43900', - ], + +export const getStatus = (theme: Theme, status: StatusValue) => { + const statusMapping: Record = { + ['status-value:unknown']: [null, null], + ['status-value:pending']: [, 'currentColor'], + ['status-value:success']: [ + + + , + 'currentColor', + ], + ['status-value:warning']: [ + + + , + theme.fgColor.warning, + ], + ['status-value:error']: [ + + + , + theme.fgColor.negative, + ], + }; + return statusMapping[status]; }; export const getMostCriticalStatusValue = (statusValues: StatusValue[]): StatusValue => { diff --git a/code/core/src/theming/base.ts b/code/core/src/theming/base.ts index 07276bed1783..676cb3f94707 100644 --- a/code/core/src/theming/base.ts +++ b/code/core/src/theming/base.ts @@ -25,7 +25,7 @@ export const color = { darkest: '#2E3338', // For borders - border: 'hsla(212, 50%, 30%, 0.15)', + border: 'hsl(212 50% 30% / 0.15)', // Status positive: '#66BF3C', @@ -100,3 +100,68 @@ export const typography = { code: 90, }, }; + +export const tokens = { + light: { + fgColor: { + default: color.darkest, + muted: color.dark, + accent: color.secondary, + inverse: color.lightest, + // TODO: add 'disabled' + positive: '#427C27', + warning: '#7A4100', + negative: '#C23400', + critical: '#FFFFFF', + }, + bgColor: { + default: color.lightest, + muted: background.app, + // TODO: add 'accent'? white or blue? + positive: '#F1FFEB', + warning: '#FFF7EB', + negative: '#FFF0EB', + critical: '#D13800', + }, + borderColor: { + default: color.border, + muted: 'hsl(0 0% 0% / 0.1)', + inverse: 'hsl(0 0% 100% / 0.1)', + positive: '#BFE7AC', + warning: '#FFCE85', + negative: '#FFC3AD', + critical: 'hsl(16 100% 100% / 0)', + }, + }, + dark: { + fgColor: { + default: '#C9CCCF', + muted: '#95999D', + accent: '#479DFF', + inverse: '#1B1C1D', + // TODO: add 'disabled' + positive: '#86CE64', + warning: '#FFAD33', + negative: '#FF6933', + critical: '#FF6933', + }, + bgColor: { + default: '#222325', + muted: '#1B1C1D', + // TODO: add 'accent'? white or blue? + positive: 'hsl(101 100% 100% / 0)', + warning: 'hsl(101 100% 100% / 0)', + negative: 'hsl(101 100% 100% / 0)', + critical: 'hsl(101 100% 100% / 0)', + }, + borderColor: { + default: 'hsl(0 0% 100% / 0.1)', + muted: 'hsl(0 0% 100% / 0.5)', + inverse: 'hsl(0 0% 0% / 0.1)', + positive: 'hsl(101 52% 64% / 0.15)', + warning: 'hsl(36 100% 64% / 0.15)', + negative: 'hsl(16 100% 64% / 0.15)', + critical: '#FF6933', + }, + }, +}; diff --git a/code/core/src/theming/convert.ts b/code/core/src/theming/convert.ts index 8cc9210ce052..b1b3c7f14449 100644 --- a/code/core/src/theming/convert.ts +++ b/code/core/src/theming/convert.ts @@ -1,7 +1,7 @@ import { opacify } from 'polished'; import { animation, easing } from './animation'; -import { background, color, typography } from './base'; +import { background, color, tokens, typography } from './base'; import { themes } from './create'; import { chromeDark, chromeLight, create as createSyntax } from './modules/syntax'; import type { Color, StorybookTheme, ThemeVars, ThemeVarsColors } from './types'; @@ -114,6 +114,9 @@ export const convert = (inherit: ThemeVars = themes[getPreferredColorScheme()]): ...rest, base, + + ...(base === 'dark' ? tokens.dark : tokens.light), + color: createColors(inherit), background: { app: appBg, @@ -127,6 +130,7 @@ export const convert = (inherit: ThemeVars = themes[getPreferredColorScheme()]): warning: background.warning, critical: background.critical, }, + typography: { fonts: { base: fontBase, @@ -175,14 +179,14 @@ export const convert = (inherit: ThemeVars = themes[getPreferredColorScheme()]): }, code: createSyntax({ - colors: base === 'light' ? lightSyntaxColors : darkSyntaxColors, + colors: base === 'dark' ? darkSyntaxColors : lightSyntaxColors, mono: fontCode, }), // Addon actions theme // API example https://github.com/storybookjs/react-inspector/blob/master/src/styles/themes/chromeLight.tsx addonActionsTheme: { - ...(base === 'light' ? chromeLight : chromeDark), + ...(base === 'dark' ? chromeDark : chromeLight), BASE_FONT_FAMILY: fontCode, BASE_FONT_SIZE: typography.size.s2 - 1, diff --git a/code/core/src/theming/create.ts b/code/core/src/theming/create.ts index 8a33d0dce577..d9b447636726 100644 --- a/code/core/src/theming/create.ts +++ b/code/core/src/theming/create.ts @@ -23,22 +23,25 @@ export const themes: Themes = { }; interface Rest { - [key: string]: any; + [key: string]: unknown; } export const create = ( - vars: ThemeVarsPartial = { base: preferredColorScheme }, + vars: ThemeVarsPartial = { + base: preferredColorScheme, + }, rest?: Rest ): ThemeVars => { + const base = themes[vars.base] ? vars.base : preferredColorScheme; const inherit: ThemeVars = { ...themes[preferredColorScheme], - ...(themes[vars.base] || {}), + ...themes[vars.base], ...vars, - ...{ base: themes[vars.base] ? vars.base : preferredColorScheme }, + base, }; return { ...rest, ...inherit, - ...{ barSelectedColor: vars.barSelectedColor || inherit.colorSecondary }, + barSelectedColor: vars.barSelectedColor || inherit.colorSecondary, }; }; diff --git a/code/core/src/theming/themes/dark.ts b/code/core/src/theming/themes/dark.ts index 7748f69dd30b..c436b26a90c5 100644 --- a/code/core/src/theming/themes/dark.ts +++ b/code/core/src/theming/themes/dark.ts @@ -1,19 +1,21 @@ -import { color, typography } from '../base'; +import { color, tokens, typography } from '../base'; import type { ThemeVars } from '../types'; +const { fgColor, bgColor, borderColor } = tokens.dark; + const theme: ThemeVars = { base: 'dark', // Storybook-specific color palette colorPrimary: '#FF4785', // coral - colorSecondary: '#479DFF', + colorSecondary: fgColor.accent, // UI - appBg: '#1B1C1D', - appContentBg: '#222325', + appBg: bgColor.muted, + appContentBg: bgColor.default, appHoverBg: '#233952', appPreviewBg: color.lightest, - appBorderColor: 'rgba(255,255,255,.1)', + appBorderColor: borderColor.default, appBorderRadius: 4, // Fonts @@ -21,24 +23,24 @@ const theme: ThemeVars = { fontCode: typography.fonts.mono, // Text colors - textColor: '#C9CCCF', - textInverseColor: '#1B1C1D', - textMutedColor: '#95999D', + textColor: fgColor.default, + textInverseColor: fgColor.inverse, + textMutedColor: fgColor.muted, // Toolbar default and active colors - barTextColor: '#95999D', + barTextColor: fgColor.muted, barHoverColor: '#70B3FF', barSelectedColor: '#479DFF', - barBg: '#222325', + barBg: bgColor.default, // Form colors - buttonBg: '#1B1C1D', - buttonBorder: 'hsl(0 0 100 / 0.1)', - booleanBg: '#1B1C1D', + buttonBg: bgColor.muted, + buttonBorder: borderColor.default, + booleanBg: bgColor.muted, booleanSelectedBg: '#292B2E', - inputBg: '#1B1C1D', - inputBorder: 'hsl(0 0 100 / 0.1)', - inputTextColor: '#C9CCCF', + inputBg: bgColor.muted, + inputBorder: borderColor.default, + inputTextColor: fgColor.default, inputBorderRadius: 4, }; diff --git a/code/core/src/theming/themes/light.ts b/code/core/src/theming/themes/light.ts index d896cb9deb51..c0e799c0fc9b 100644 --- a/code/core/src/theming/themes/light.ts +++ b/code/core/src/theming/themes/light.ts @@ -1,19 +1,21 @@ -import { background, color, typography } from '../base'; +import { background, color, tokens, typography } from '../base'; import type { ThemeVars } from '../types'; +const { fgColor, bgColor, borderColor } = tokens.light; + const theme: ThemeVars = { base: 'light', // Storybook-specific color palette colorPrimary: color.primary, - colorSecondary: color.secondary, + colorSecondary: fgColor.accent, // UI - appBg: background.app, - appContentBg: color.lightest, + appBg: bgColor.muted, + appContentBg: bgColor.default, appHoverBg: '#DBECFF', - appPreviewBg: color.lightest, - appBorderColor: color.border, + appPreviewBg: bgColor.default, + appBorderColor: borderColor.default, appBorderRadius: 4, // Fonts @@ -21,24 +23,24 @@ const theme: ThemeVars = { fontCode: typography.fonts.mono, // Text colors - textColor: color.darkest, - textInverseColor: color.lightest, - textMutedColor: color.dark, + textColor: fgColor.default, + textInverseColor: fgColor.inverse, + textMutedColor: fgColor.muted, // Toolbar default and active colors - barTextColor: color.dark, + barTextColor: fgColor.muted, barHoverColor: '#005CC7', barSelectedColor: '#0063D6', - barBg: color.lightest, + barBg: bgColor.default, // Form colors - buttonBg: background.app, + buttonBg: bgColor.muted, buttonBorder: color.medium, booleanBg: color.mediumlight, - booleanSelectedBg: color.lightest, - inputBg: color.lightest, - inputBorder: color.border, - inputTextColor: color.darkest, + booleanSelectedBg: bgColor.default, + inputBg: bgColor.default, + inputBorder: borderColor.default, + inputTextColor: fgColor.default, inputBorderRadius: 4, }; diff --git a/code/core/src/theming/types.ts b/code/core/src/theming/types.ts index f009df790df8..9a58a3ed2a29 100644 --- a/code/core/src/theming/types.ts +++ b/code/core/src/theming/types.ts @@ -1,5 +1,5 @@ import type { animation, easing } from './animation'; -import type { background, color, typography } from './base'; +import type { background, color, tokens, typography } from './base'; export interface ThemeVars extends ThemeVarsBase, ThemeVarsColors {} @@ -70,6 +70,11 @@ export interface Brand { export interface StorybookTheme { color: Color; + + fgColor: typeof tokens.light.fgColor; + bgColor: typeof tokens.light.bgColor; + borderColor: typeof tokens.light.borderColor; + background: Background; typography: Typography; animation: Animation;