diff --git a/CHANGELOG.prerelease.md b/CHANGELOG.prerelease.md index 47b4af1e1323..fa9c9e593a0f 100644 --- a/CHANGELOG.prerelease.md +++ b/CHANGELOG.prerelease.md @@ -1,3 +1,12 @@ +## 10.1.0-beta.3 + +- A11y: Make search clear button keyboard accessible - [#32590](https://github.com/storybookjs/storybook/pull/32590), thanks @ritoban23! +- Angular: Add preset entry point for framework - [#33154](https://github.com/storybookjs/storybook/pull/33154), thanks @valentinpalkovic! +- CLI: Fix framework config validation path and messages - [#33146](https://github.com/storybookjs/storybook/pull/33146), thanks @valentinpalkovic! +- Manager: Added tokens and a dark color scheme for status colors - [#33081](https://github.com/storybookjs/storybook/pull/33081), thanks @MichaelArestad! +- Remove yarn esbuild pnp plugin - [#33097](https://github.com/storybookjs/storybook/pull/33097), thanks @mrginglymus! +- UI: Increase border contrast of Checkbox, Radio, and Range - [#33064](https://github.com/storybookjs/storybook/pull/33064), thanks @MichaelArestad! + ## 10.1.0-beta.2 - Automigration: Update description and link for addon-a11y-addon-test - [#33133](https://github.com/storybookjs/storybook/pull/33133), thanks @valentinpalkovic! diff --git a/code/addons/docs/src/blocks/controls/Range.tsx b/code/addons/docs/src/blocks/controls/Range.tsx index 5baea1088edb..28f0149cdc21 100644 --- a/code/addons/docs/src/blocks/controls/Range.tsx +++ b/code/addons/docs/src/blocks/controls/Range.tsx @@ -11,148 +11,128 @@ import type { ControlProps, NumberValue, RangeConfig } from './types'; type RangeProps = ControlProps & RangeConfig; const RangeInput = styled.input<{ min: number; max: number; value: number }>( - ({ theme, min, max, value, disabled }) => ({ - // Resytled using http://danielstern.ca/range.css/#/ - '&': { - width: '100%', - backgroundColor: 'transparent', - appearance: 'none', - }, - - '&::-webkit-slider-runnable-track': { - background: - theme.base === 'light' - ? `linear-gradient(to right, - ${theme.color.green} 0%, ${theme.color.green} ${((value - min) / (max - min)) * 100}%, - ${darken(0.02, theme.input.background)} ${((value - min) / (max - min)) * 100}%, - ${darken(0.02, theme.input.background)} 100%)` - : `linear-gradient(to right, - ${theme.color.green} 0%, ${theme.color.green} ${((value - min) / (max - min)) * 100}%, - ${lighten(0.02, theme.input.background)} ${((value - min) / (max - min)) * 100}%, - ${lighten(0.02, theme.input.background)} 100%)`, - boxShadow: `${theme.appBorderColor} 0 0 0 1px inset`, + ({ theme, min, max, value, disabled }) => { + // Shared track background gradient + const trackBackground = + theme.base === 'light' + ? `linear-gradient(to right, + ${theme.color.green} 0%, ${theme.color.green} ${((value - min) / (max - min)) * 100}%, + ${darken(0.02, theme.input.background)} ${((value - min) / (max - min)) * 100}%, + ${darken(0.02, theme.input.background)} 100%)` + : `linear-gradient(to right, + ${theme.color.green} 0%, ${theme.color.green} ${((value - min) / (max - min)) * 100}%, + ${lighten(0.02, theme.input.background)} ${((value - min) / (max - min)) * 100}%, + ${lighten(0.02, theme.input.background)} 100%)`; + + // Shared track base styles + const trackBaseStyles = { + background: trackBackground, borderRadius: 6, - width: '100%', - height: 6, + boxShadow: `${theme.base == 'dark' ? 'hsl(0 0 100 / 0.4)' : 'hsl(0 0 0 / 0.44)'} 0 0 0 1px inset`, cursor: disabled ? 'not-allowed' : 'pointer', - }, + height: 6, + width: '100%', + }; + + const trackFocusStyles = { + borderColor: rgba(theme.color.secondary, 0.4), + }; - '&::-webkit-slider-thumb': { - marginTop: '-6px', + // Shared thumb base styles + const thumbBaseStyles = { width: 16, height: 16, - - border: `1px solid ${theme.appBorderColor}`, - borderRadius: '50px', - boxShadow: - theme.base === 'light' ? `0 1px 3px 0px ${rgba(theme.appBorderColor, 0.2)}` : 'unset', + borderRadius: 50, cursor: disabled ? 'not-allowed' : 'grab', - appearance: 'none', background: theme.input.background, + border: `1px solid ${theme.base == 'dark' ? 'hsl(0 0 100 / 0.4)' : 'hsl(0 0 0 / 0.44)'}`, + boxShadow: + theme.base === 'light' ? `0 1px 3px 0px ${rgba(theme.appBorderColor, 0.2)}` : 'unset', transition: 'all 150ms ease-out', + }; + + // Shared thumb hover styles + const thumbHoverStyles = { + background: `${darken(0.05, theme.input.background)}`, + transform: 'scale3d(1.1, 1.1, 1.1) translateY(-1px)', + transition: 'all 50ms ease-out', + }; + + // Shared thumb active styles + const thumbActiveStyles = { + background: `${theme.input.background}`, + transform: 'scale3d(1, 1, 1) translateY(0px)', + }; + + const thumbFocusStyles = { + borderColor: theme.color.secondary, + boxShadow: theme.base === 'light' ? `0 0px 5px 0px ${theme.color.secondary}` : 'unset', + }; + + return { + // Restyled using http://danielstern.ca/range.css/#/ + appearance: 'none', + backgroundColor: 'transparent', + width: '100%', - '&:hover': { - background: `${darken(0.05, theme.input.background)}`, - transform: 'scale3d(1.1, 1.1, 1.1) translateY(-1px)', - transition: 'all 50ms ease-out', - }, + // Track styles + '&::-webkit-slider-runnable-track': trackBaseStyles, + + '&::-moz-range-track': trackBaseStyles, - '&:active': { - background: `${theme.input.background}`, - transform: 'scale3d(1, 1, 1) translateY(0px)', - cursor: disabled ? 'not-allowed' : 'grab', + '&::-ms-track': { + ...trackBaseStyles, + color: 'transparent', }, - }, - '&:focus': { - outline: 'none', + // Thumb styles + '&::-moz-range-thumb': { + ...thumbBaseStyles, - '&::-webkit-slider-runnable-track': { - borderColor: rgba(theme.color.secondary, 0.4), + '&:hover': thumbHoverStyles, + '&:active': thumbActiveStyles, }, '&::-webkit-slider-thumb': { - borderColor: theme.color.secondary, - boxShadow: theme.base === 'light' ? `0 0px 5px 0px ${theme.color.secondary}` : 'unset', + ...thumbBaseStyles, + marginTop: '-6px', + appearance: 'none', + + '&:hover': thumbHoverStyles, + '&:active': thumbActiveStyles, }, - }, - - '&::-moz-range-track': { - background: - theme.base === 'light' - ? `linear-gradient(to right, - ${theme.color.green} 0%, ${theme.color.green} ${((value - min) / (max - min)) * 100}%, - ${darken(0.02, theme.input.background)} ${((value - min) / (max - min)) * 100}%, - ${darken(0.02, theme.input.background)} 100%)` - : `linear-gradient(to right, - ${theme.color.green} 0%, ${theme.color.green} ${((value - min) / (max - min)) * 100}%, - ${lighten(0.02, theme.input.background)} ${((value - min) / (max - min)) * 100}%, - ${lighten(0.02, theme.input.background)} 100%)`, - boxShadow: `${theme.appBorderColor} 0 0 0 1px inset`, - borderRadius: 6, - width: '100%', - height: 6, - cursor: disabled ? 'not-allowed' : 'pointer', - outline: 'none', - }, - '&::-moz-range-thumb': { - width: 16, - height: 16, - border: `1px solid ${theme.appBorderColor}`, - borderRadius: '50px', - boxShadow: - theme.base === 'light' ? `0 1px 3px 0px ${rgba(theme.appBorderColor, 0.2)}` : 'unset', - cursor: disabled ? 'not-allowed' : 'grab', - background: theme.input.background, - transition: 'all 150ms ease-out', + '&::-ms-thumb': { + ...thumbBaseStyles, + marginTop: 0, - '&:hover': { - background: `${darken(0.05, theme.input.background)}`, - transform: 'scale3d(1.1, 1.1, 1.1) translateY(-1px)', - transition: 'all 50ms ease-out', + '&:hover': thumbHoverStyles, + '&:active': thumbActiveStyles, }, - '&:active': { - background: `${theme.input.background}`, - transform: 'scale3d(1, 1, 1) translateY(0px)', - cursor: 'grabbing', + '&:focus': { + outline: 'none', + + '&::-webkit-slider-runnable-track': trackFocusStyles, + '&::-moz-range-track': trackFocusStyles, + '&::-ms-track': trackFocusStyles, + + '&::-webkit-slider-thumb': thumbFocusStyles, + '&::-moz-range-thumb': thumbFocusStyles, + '&::-ms-thumb': thumbFocusStyles, }, - }, - '&::-ms-track': { - background: - theme.base === 'light' - ? `linear-gradient(to right, - ${theme.color.green} 0%, ${theme.color.green} ${((value - min) / (max - min)) * 100}%, - ${darken(0.02, theme.input.background)} ${((value - min) / (max - min)) * 100}%, - ${darken(0.02, theme.input.background)} 100%)` - : `linear-gradient(to right, - ${theme.color.green} 0%, ${theme.color.green} ${((value - min) / (max - min)) * 100}%, - ${lighten(0.02, theme.input.background)} ${((value - min) / (max - min)) * 100}%, - ${lighten(0.02, theme.input.background)} 100%)`, - boxShadow: theme.base === 'light' ? `${theme.appBorderColor} 0 0 0 1px inset` : 'unset', - color: 'transparent', - width: '100%', - height: '6px', - cursor: 'pointer', - }, - '&::-ms-fill-lower': { - borderRadius: 6, - }, - '&::-ms-fill-upper': { - borderRadius: 6, - }, - '&::-ms-thumb': { - width: 16, - height: 16, - background: theme.input.background, - border: `1px solid ${rgba(theme.appBorderColor, 0.2)}`, - borderRadius: 50, - cursor: disabled ? 'not-allowed' : 'grab', - marginTop: 0, - }, - '@supports (-ms-ime-align:auto)': { 'input[type=range]': { margin: '0' } }, - }) + + '&::-ms-fill-lower': { + borderRadius: 6, + }, + + '&::-ms-fill-upper': { + borderRadius: 6, + }, + + '@supports (-ms-ime-align:auto)': { 'input[type=range]': { margin: '0' } }, + }; + } ); const RangeLabel = styled.span({ diff --git a/code/core/package.json b/code/core/package.json index 04eb0eae9ff3..e8f6e3b21cbb 100644 --- a/code/core/package.json +++ b/code/core/package.json @@ -261,7 +261,6 @@ "@types/semver": "^7.5.8", "@types/ws": "^8", "@vitest/utils": "^3.2.4", - "@yarnpkg/esbuild-plugin-pnp": "^3.0.0-rc.10", "@yarnpkg/fslib": "2.10.3", "@yarnpkg/libzip": "2.3.0", "ansi-to-html": "^0.7.2", diff --git a/code/core/src/builder-manager/index.ts b/code/core/src/builder-manager/index.ts index 7daba1650222..265b37e102d8 100644 --- a/code/core/src/builder-manager/index.ts +++ b/code/core/src/builder-manager/index.ts @@ -4,8 +4,6 @@ import { stringifyProcessEnvs } from 'storybook/internal/common'; import { logger } from 'storybook/internal/node-logger'; import { globalExternals } from '@fal-works/esbuild-plugin-global-externals'; -// TODO: Remove in SB11 -import { pnpPlugin } from '@yarnpkg/esbuild-plugin-pnp'; import { resolveModulePath } from 'exsolve'; import { join, parse } from 'pathe'; import sirv from 'sirv'; @@ -104,7 +102,7 @@ export const getConfig: ManagerBuilder['getConfig'] = async (options) => { tsconfig: tsconfigPath, legalComments: 'external', - plugins: [globalExternals(globalsModuleInfoMap), pnpPlugin()], + plugins: [globalExternals(globalsModuleInfoMap)], banner: { js: 'try{', diff --git a/code/core/src/common/utils/validate-config.ts b/code/core/src/common/utils/validate-config.ts index e8c3de208e73..a0b401bf1133 100644 --- a/code/core/src/common/utils/validate-config.ts +++ b/code/core/src/common/utils/validate-config.ts @@ -6,6 +6,7 @@ import { import { resolveModulePath } from 'exsolve'; +import { extractFrameworkPackageName } from '..'; import { frameworkPackages } from './get-storybook-info'; const renderers = ['html', 'preact', 'react', 'server', 'svelte', 'vue', 'vue3', 'web-components']; @@ -27,7 +28,8 @@ export function validateFrameworkName( } // If we know about the framework, we don't need to validate it - if (Object.keys(frameworkPackages).includes(frameworkName)) { + const normalizedFrameworkName = extractFrameworkPackageName(frameworkName); + if (Object.keys(frameworkPackages).includes(normalizedFrameworkName)) { return; } 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/components/components/Form/Checkbox.tsx b/code/core/src/components/components/Form/Checkbox.tsx index aadbd979e9f9..f298e1db5115 100644 --- a/code/core/src/components/components/Form/Checkbox.tsx +++ b/code/core/src/components/components/Form/Checkbox.tsx @@ -4,27 +4,29 @@ import { color, styled } from 'storybook/theming'; const Input = styled.input(({ theme }) => ({ appearance: 'none', + backgroundColor: theme.input.background, + border: `1px solid ${theme.base === 'dark' ? 'hsl(0 0 100 / 0.4)' : 'hsl(0 0 0 / 0.44)'}`, + borderRadius: 2, display: 'grid', - placeContent: 'center', - width: 14, - height: 14, flexShrink: 0, + height: 14, margin: 0, - border: `1px solid ${theme.input.border}`, - borderRadius: 2, - backgroundColor: theme.input.background, + placeContent: 'center', transition: 'background-color 0.1s', + width: 14, '&:enabled': { cursor: 'pointer', }, '&:disabled': { - backgroundColor: theme.base === 'light' ? color.light : 'transparent', + backgroundColor: 'transparent', + borderColor: theme.input.border, }, '&:disabled:checked, &:disabled:indeterminate': { - backgroundColor: theme.base === 'light' ? color.mediumdark : theme.color.dark, + backgroundColor: theme.base === 'dark' ? color.dark : theme.color.mediumdark, }, '&:checked, &:indeterminate': { + border: 'none', backgroundColor: color.secondary, }, '&:checked::before': { diff --git a/code/core/src/components/components/Form/Radio.tsx b/code/core/src/components/components/Form/Radio.tsx index 2ea6e9cd7310..53a3c8759e84 100644 --- a/code/core/src/components/components/Form/Radio.tsx +++ b/code/core/src/components/components/Form/Radio.tsx @@ -4,28 +4,31 @@ import { color, styled } from 'storybook/theming'; const Input = styled.input(({ theme }) => ({ appearance: 'none', + backgroundColor: theme.input.background, + border: `1px solid ${theme.base === 'dark' ? 'hsl(0 0 100 / 0.4)' : 'hsl(0 0 0 / 0.44)'}`, + borderRadius: 8, display: 'grid', - placeContent: 'center', - width: 16, - height: 16, flexShrink: 0, + height: 16, margin: -1, - border: `1px solid ${theme.input.border}`, - borderRadius: 8, - backgroundColor: theme.input.background, + placeContent: 'center', transition: 'background-color 0.1s', + width: 16, '&:enabled': { cursor: 'pointer', }, '&:disabled': { - backgroundColor: theme.base === 'light' ? color.light : 'transparent', + backgroundColor: 'transparent', + borderColor: theme.input.border, }, '&:disabled:checked': { - backgroundColor: theme.base === 'light' ? color.light : theme.color.mediumdark, + backgroundColor: theme.base === 'dark' ? color.dark : theme.color.mediumdark, + borderColor: theme.base === 'dark' ? color.dark : theme.color.mediumdark, }, '&:checked': { backgroundColor: color.secondary, + borderColor: color.secondary, boxShadow: `inset 0 0 0 2px ${theme.input.background}`, }, '&:enabled:focus-visible': { diff --git a/code/core/src/manager/components/sidebar/Search.tsx b/code/core/src/manager/components/sidebar/Search.tsx index ede9c9376755..b5f955b60b2b 100644 --- a/code/core/src/manager/components/sidebar/Search.tsx +++ b/code/core/src/manager/components/sidebar/Search.tsx @@ -328,13 +328,13 @@ export const Search = React.memo(function Search({ openMenu, closeMenu, inputValue, - clearSelection, getInputProps, getItemProps, getLabelProps, getMenuProps, getRootProps, highlightedIndex, + reset, }) => { const input = inputValue ? inputValue.trim() : ''; let results: DownshiftItem[] = input ? getResults(input) : []; @@ -415,7 +415,10 @@ export const Search = React.memo(function Search({ padding="small" variant="ghost" ariaLabel="Clear search" - onClick={() => clearSelection()} + onClick={() => { + reset({ inputValue: '' }); + closeMenu(); + }} > diff --git a/code/core/src/manager/components/sidebar/SearchResults.tsx b/code/core/src/manager/components/sidebar/SearchResults.tsx index d349406c55cf..91256cd50cd6 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'; @@ -158,6 +158,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(); @@ -171,12 +172,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 a011c18b8312..0d0862c72943 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, @@ -214,6 +214,7 @@ const Node = React.memo(function Node(props) { onSelectStoryId, api, } = props; + const theme = useTheme(); const { isDesktop, isMobile, setMobileMenuOpen } = useLayout(); const { counts, statusesByValue } = useStatusSummary(item); @@ -282,6 +283,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 ( @@ -454,7 +520,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 044da1a20654..ec915c414def 100644 --- a/code/core/src/manager/globals/exports.ts +++ b/code/core/src/manager/globals/exports.ts @@ -381,6 +381,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..7a0d1179de53 100644 --- a/code/core/src/manager/utils/status.tsx +++ b/code/core/src/manager/utils/status.tsx @@ -6,7 +6,8 @@ import { type API_HashEntry, type StatusesByStoryIdAndTypeId } from 'storybook/i import { CircleIcon } from '@storybook/icons'; -import { styled } from 'storybook/theming'; +import memoizerific from 'memoizerific'; +import { type Theme, styled } from 'storybook/theming'; import { UseSymbol } from '../components/sidebar/IconSymbols'; import { getDescendantIds } from './tree'; @@ -31,28 +32,35 @@ 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', - ], -}; + +// We might not want to make this a hook because it is used in the Tree after multiple returns. +// There could be scenarios where creating a story changes the type of an item (e.g. story now +// has children because it has a test child), so we could end up with rule of hooks violations. +export const getStatus = memoizerific(5)((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 => { return statusPriority.reduce( 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..5167fa191818 100644 --- a/code/core/src/theming/create.ts +++ b/code/core/src/theming/create.ts @@ -23,22 +23,28 @@ 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 inherit: ThemeVars = { + // We always inherit the preferred color scheme. ...themes[preferredColorScheme], + // And then the declared theme base if it exists. ...(themes[vars.base] || {}), + // And then the actual theme content. ...vars, - ...{ base: themes[vars.base] ? vars.base : preferredColorScheme }, + // If no theme base was declared, we declare the preferred color scheme as the base. + base: themes[vars.base] ? vars.base : preferredColorScheme, }; return { ...rest, ...inherit, - ...{ barSelectedColor: vars.barSelectedColor || inherit.colorSecondary }, + barSelectedColor: vars.barSelectedColor || inherit.colorSecondary, }; }; diff --git a/code/core/src/theming/tests/convert.test.js b/code/core/src/theming/tests/convert.test.js index 6cfa924323a7..72955e843129 100644 --- a/code/core/src/theming/tests/convert.test.js +++ b/code/core/src/theming/tests/convert.test.js @@ -18,7 +18,7 @@ describe('convert', () => { expect(result).toMatchObject({ color: expect.objectContaining({ ancillary: '#22a699', - border: 'hsla(212, 50%, 30%, 0.15)', + border: 'hsl(212 50% 30% / 0.15)', critical: '#FFFFFF', dark: '#5C6570', darker: '#454C54', @@ -68,7 +68,7 @@ describe('convert', () => { expect(result).toMatchObject({ color: expect.objectContaining({ ancillary: '#22a699', - border: 'hsla(212, 50%, 30%, 0.15)', + border: 'hsl(212 50% 30% / 0.15)', critical: '#FFFFFF', dark: '#5C6570', darker: '#454C54', diff --git a/code/core/src/theming/themes/dark.ts b/code/core/src/theming/themes/dark.ts index 7748f69dd30b..e2aa337335d0 100644 --- a/code/core/src/theming/themes/dark.ts +++ b/code/core/src/theming/themes/dark.ts @@ -13,7 +13,7 @@ const theme: ThemeVars = { appContentBg: '#222325', appHoverBg: '#233952', appPreviewBg: color.lightest, - appBorderColor: 'rgba(255,255,255,.1)', + appBorderColor: 'hsl(0 0% 100% / 0.1)', appBorderRadius: 4, // Fonts @@ -33,11 +33,11 @@ const theme: ThemeVars = { // Form colors buttonBg: '#1B1C1D', - buttonBorder: 'hsl(0 0 100 / 0.1)', + buttonBorder: 'hsl(0 0% 100% / 0.1)', booleanBg: '#1B1C1D', booleanSelectedBg: '#292B2E', inputBg: '#1B1C1D', - inputBorder: 'hsl(0 0 100 / 0.1)', + inputBorder: 'hsl(0 0% 100% / 0.1)', inputTextColor: '#C9CCCF', 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; diff --git a/code/frameworks/angular/preset.js b/code/frameworks/angular/preset.js new file mode 100644 index 000000000000..4bd63d324002 --- /dev/null +++ b/code/frameworks/angular/preset.js @@ -0,0 +1 @@ +export * from './dist/preset.js'; diff --git a/code/package.json b/code/package.json index 2d42d1fcbeee..60b62440f25b 100644 --- a/code/package.json +++ b/code/package.json @@ -286,5 +286,6 @@ "Dependency Upgrades" ] ] - } + }, + "deferredNextVersion": "10.1.0-beta.3" } diff --git a/code/yarn.lock b/code/yarn.lock index 2ff72d53b356..1d672ebd3d2b 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -10824,17 +10824,6 @@ __metadata: languageName: node linkType: hard -"@yarnpkg/esbuild-plugin-pnp@npm:^3.0.0-rc.10": - version: 3.0.0-rc.15 - resolution: "@yarnpkg/esbuild-plugin-pnp@npm:3.0.0-rc.15" - dependencies: - tslib: "npm:^2.4.0" - peerDependencies: - esbuild: ">=0.10.0" - checksum: 10c0/5095bc316862971add31ca1fadb0095b6ad15f25120f6ab3a06086bb6a7be93c2f3c45bff80d5976689fc89b0e9bf82bd3d410e205c852739874d32d050c4e57 - languageName: node - linkType: hard - "@yarnpkg/fslib@npm:2.10.3": version: 2.10.3 resolution: "@yarnpkg/fslib@npm:2.10.3" @@ -25936,7 +25925,6 @@ __metadata: "@vitest/expect": "npm:3.2.4" "@vitest/spy": "npm:3.2.4" "@vitest/utils": "npm:^3.2.4" - "@yarnpkg/esbuild-plugin-pnp": "npm:^3.0.0-rc.10" "@yarnpkg/fslib": "npm:2.10.3" "@yarnpkg/libzip": "npm:2.3.0" ansi-to-html: "npm:^0.7.2" diff --git a/docs/api/csf/index.mdx b/docs/api/csf/index.mdx index 50afb12d6183..b0a50147f1f4 100644 --- a/docs/api/csf/index.mdx +++ b/docs/api/csf/index.mdx @@ -9,7 +9,7 @@ tab: title: CSF 3 --- -Component Story Format (CSF) is the recommended way to [write stories](../../writing-stories/index.mdx). It's an [open standard](https://github.com/ComponentDriven/csf) based on ES6 modules that is portable beyond Storybook. +Component Story Format (CSF) is the recommended way to [write stories](../../writing-stories/index.mdx). It's an open standard based on ES6 modules that is portable beyond Storybook. In CSF, stories and component metadata are defined as ES Modules. Every component story file consists of a required [default export](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export#Using_the_default_export) and one or more [named exports](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export). diff --git a/docs/api/index.mdx b/docs/api/index.mdx index 5a957e12bf3a..7fcbe13736fe 100644 --- a/docs/api/index.mdx +++ b/docs/api/index.mdx @@ -80,7 +80,7 @@ An overview of all available API references for Storybook. Component Story Format (CSF) is the API for writing stories. It's an - open standard based on ES6 modules that + open standard based on ES6 modules that is portable beyond Storybook. diff --git a/docs/get-started/why-storybook.mdx b/docs/get-started/why-storybook.mdx index df8269db386a..887d98dad48b 100644 --- a/docs/get-started/why-storybook.mdx +++ b/docs/get-started/why-storybook.mdx @@ -29,7 +29,7 @@ Storybook is packaged as a small, development-only, [workshop](https://bradfrost ### Capture UI variations as “stories” -When developing a component variation in isolation, save it as a story. [Stories](https://github.com/ComponentDriven/csf) are a declarative syntax for supplying props and mock data to simulate component variations. Each component can have multiple stories. Each story allows you to demonstrate a specific variation of that component to verify appearance and behavior. +When developing a component variation in isolation, save it as a story. [Stories](../writing-stories/index.mdx) are a declarative syntax for supplying props and mock data to simulate component variations. Each component can have multiple stories. Each story allows you to demonstrate a specific variation of that component to verify appearance and behavior. You write stories for granular UI component variation and then use those stories in development, testing, and documentation. @@ -100,7 +100,7 @@ Storybook is compatible with your continuous integration workflow. Add it as a C ## Write stories once, reuse everywhere -Storybook is powered by [Component Story Format](https://github.com/ComponentDriven/csf), an open standard based on JavaScript ES6 modules. This enables stories to interoperate between development, testing, and design tools. Each story is exported as a JavaScript function enabling you to reuse it with other tools. No vendor lock-in. +Storybook is powered by [Component Story Format](../api/csf/index.mdx), an open standard based on JavaScript ES6 modules. This enables stories to interoperate between development, testing, and design tools. Each story is exported as a JavaScript function enabling you to reuse it with other tools. No vendor lock-in. Reuse stories with [Jest](https://jestjs.io/) or [Vitest](https://vitest.dev/) and [Testing Library](https://testing-library.com/) to verify interactions. Put them in [Chromatic](https://www.chromatic.com/?utm_source=storybook_website\&utm_medium=link\&utm_campaign=storybook) for visual testing. Audit story accessibility with [Axe](https://github.com/dequelabs/axe-core). Or test user flows with [Playwright](https://playwright.dev/) and [Cypress](https://www.cypress.io/). Reuse unlocks more workflows at no extra cost. diff --git a/docs/versions/next.json b/docs/versions/next.json index eade4140dec0..0e951cd74e58 100644 --- a/docs/versions/next.json +++ b/docs/versions/next.json @@ -1 +1 @@ -{"version":"10.1.0-beta.2","info":{"plain":"- Automigration: Update description and link for addon-a11y-addon-test - [#33133](https://github.com/storybookjs/storybook/pull/33133), thanks @valentinpalkovic!\n- CLI: Fix Vitest v3 installs and refactor AddonVitestService; align create‑storybook usage - [#33131](https://github.com/storybookjs/storybook/pull/33131), thanks @valentinpalkovic!\n- CLI: Update postAction hook to use command parameter for logfile retrieval - [#33137](https://github.com/storybookjs/storybook/pull/33137), thanks @valentinpalkovic!\n- Core: Fix `getDocsUrl` for canary versions - [#33128](https://github.com/storybookjs/storybook/pull/33128), thanks @ghengeveld!"}} \ No newline at end of file +{"version":"10.1.0-beta.3","info":{"plain":"- A11y: Make search clear button keyboard accessible - [#32590](https://github.com/storybookjs/storybook/pull/32590), thanks @ritoban23!\n- Angular: Add preset entry point for framework - [#33154](https://github.com/storybookjs/storybook/pull/33154), thanks @valentinpalkovic!\n- CLI: Fix framework config validation path and messages - [#33146](https://github.com/storybookjs/storybook/pull/33146), thanks @valentinpalkovic!\n- Manager: Added tokens and a dark color scheme for status colors - [#33081](https://github.com/storybookjs/storybook/pull/33081), thanks @MichaelArestad!\n- Remove yarn esbuild pnp plugin - [#33097](https://github.com/storybookjs/storybook/pull/33097), thanks @mrginglymus!\n- UI: Increase border contrast of Checkbox, Radio, and Range - [#33064](https://github.com/storybookjs/storybook/pull/33064), thanks @MichaelArestad!"}} \ No newline at end of file