mirror of
https://github.com/storybookjs/storybook.git
synced 2025-04-05 06:01:22 +08:00
Merge branch 'next' into docs_fix_snippets_v3
This commit is contained in:
commit
cbdfcb5264
@ -980,22 +980,22 @@ workflows:
|
||||
requires:
|
||||
- build
|
||||
- create-sandboxes:
|
||||
parallelism: 37
|
||||
parallelism: 38
|
||||
requires:
|
||||
- build
|
||||
# - smoke-test-sandboxes: # disabled for now
|
||||
# requires:
|
||||
# - create-sandboxes
|
||||
- build-sandboxes:
|
||||
parallelism: 37
|
||||
parallelism: 38
|
||||
requires:
|
||||
- create-sandboxes
|
||||
- chromatic-sandboxes:
|
||||
parallelism: 34
|
||||
parallelism: 35
|
||||
requires:
|
||||
- build-sandboxes
|
||||
- e2e-production:
|
||||
parallelism: 32
|
||||
parallelism: 33
|
||||
requires:
|
||||
- build-sandboxes
|
||||
- e2e-dev:
|
||||
@ -1003,7 +1003,7 @@ workflows:
|
||||
requires:
|
||||
- create-sandboxes
|
||||
- test-runner-production:
|
||||
parallelism: 32
|
||||
parallelism: 33
|
||||
requires:
|
||||
- build-sandboxes
|
||||
- vitest-integration:
|
||||
|
@ -1,3 +1,10 @@
|
||||
## 8.4.5
|
||||
|
||||
- Angular: Support v19 - [#29659](https://github.com/storybookjs/storybook/pull/29659), thanks @leosvelperez!
|
||||
- CLI: Disable corepack auto pin behavior - [#29627](https://github.com/storybookjs/storybook/pull/29627), thanks @yannbf!
|
||||
- CLI: Fix qwik init - [#29632](https://github.com/storybookjs/storybook/pull/29632), thanks @shilman!
|
||||
- Nextjs-Vite: Add Next.js 15 support - [#29640](https://github.com/storybookjs/storybook/pull/29640), thanks @yannbf!
|
||||
|
||||
## 8.4.4
|
||||
|
||||
- Addon Test: Only optimize react deps if applicable in vitest-plugin - [#29617](https://github.com/storybookjs/storybook/pull/29617), thanks @yannbf!
|
||||
|
@ -1,3 +1,30 @@
|
||||
## 8.5.0-alpha.9
|
||||
|
||||
- Angular: Support v19 - [#29659](https://github.com/storybookjs/storybook/pull/29659), thanks @leosvelperez!
|
||||
- Manager: Fix size regression - [#29660](https://github.com/storybookjs/storybook/pull/29660), thanks @JReinhold!
|
||||
- Nextjs-Vite: Add Next.js 15 support - [#29640](https://github.com/storybookjs/storybook/pull/29640), thanks @yannbf!
|
||||
|
||||
## 8.5.0-alpha.8
|
||||
|
||||
- UI: Sidebar context menu addon API - [#29557](https://github.com/storybookjs/storybook/pull/29557), thanks @ndelangen!
|
||||
|
||||
## 8.5.0-alpha.7
|
||||
|
||||
- CLI: Disable corepack auto pin behavior - [#29627](https://github.com/storybookjs/storybook/pull/29627), thanks @yannbf!
|
||||
- RNW-Vite: Integrate with experimental-addon-test - [#29645](https://github.com/storybookjs/storybook/pull/29645), thanks @shilman!
|
||||
|
||||
## 8.5.0-alpha.6
|
||||
|
||||
- CLI: Fix qwik init - [#29632](https://github.com/storybookjs/storybook/pull/29632), thanks @shilman!
|
||||
- React Native Web: Add framework, CLI integration, sandboxes - [#29520](https://github.com/storybookjs/storybook/pull/29520), thanks @shilman!
|
||||
|
||||
## 8.5.0-alpha.5
|
||||
|
||||
- Addon Test: Only optimize react deps if applicable in vitest-plugin - [#29617](https://github.com/storybookjs/storybook/pull/29617), thanks @yannbf!
|
||||
- Addon Test: Optimize internal dependencies - [#29595](https://github.com/storybookjs/storybook/pull/29595), thanks @yannbf!
|
||||
- CLI: Fix init help for `storybook` command - [#29480](https://github.com/storybookjs/storybook/pull/29480), thanks @toothlessdev!
|
||||
- Composition: Fix composed story search - [#29453](https://github.com/storybookjs/storybook/pull/29453), thanks @jsingh0026!
|
||||
|
||||
## 8.5.0-alpha.4
|
||||
|
||||
- Next.js: Add support for Next 15 - [#29587](https://github.com/storybookjs/storybook/pull/29587), thanks @yannbf!
|
||||
|
@ -36,7 +36,7 @@
|
||||
<a href="#sponsors">
|
||||
<img src="https://opencollective.com/storybook/tiers/sponsors/badge.svg" alt="Sponsors on Open Collective" />
|
||||
</a>
|
||||
<a href="https://twitter.com/intent/follow?screen_name=storybookjs">
|
||||
<a href="https://x.com/intent/follow?screen_name=storybookjs">
|
||||
<img src="https://img.shields.io/twitter/follow/storybookjs?color=blue&logo=twitter" alt="Official Twitter Handle" />
|
||||
</a>
|
||||
<a href="https://api.securityscorecards.dev/projects/github.com/storybookjs/storybook">
|
||||
@ -68,8 +68,8 @@ Storybook is a frontend workshop for building UI components and pages in isolati
|
||||
- 👥 [Community](#community)
|
||||
- 👏 [Contributing](#contributing)
|
||||
- 👨💻 [Development scripts](#development-scripts)
|
||||
- 💵 [Backers](#backers)
|
||||
- 💸 [Sponsors](#sponsors)
|
||||
- 💵 [Backers](#backers)
|
||||
- :memo: [License](#license)
|
||||
|
||||
## Getting Started
|
||||
@ -156,7 +156,7 @@ If you're looking for material to use in your Storybook presentation, such as lo
|
||||
|
||||
## Community
|
||||
|
||||
- Tweeting via [@storybookjs](https://twitter.com/storybookjs)
|
||||
- Tweeting via [@storybookjs](https://x.com/storybookjs)
|
||||
- Blogging at [storybook.js.org](https://storybook.js.org/blog/) and [Medium](https://medium.com/storybookjs)
|
||||
- Chatting on [Discord](https://discord.gg/storybook)
|
||||
- Videos and streams at [YouTube](https://www.youtube.com/channel/UCr7Quur3eIyA_oe8FNYexfg)
|
||||
|
@ -2,10 +2,6 @@
|
||||
|
||||
This file keeps track of any resolutions or exact versions specified in any `package.json` file. Resolutions are used to specify a specific version of a package to be used, even if a different version is specified as a dependency of another package.
|
||||
|
||||
## code/renderers/svelte/package.json
|
||||
## path/to/package.json
|
||||
|
||||
svelte-check@3.4.6 (bug: 3.5.x): Type issues
|
||||
|
||||
## code/ui/components/package.json
|
||||
|
||||
overlayscrollbars@2.2.1 (bug: 2.3.x): The Scrollbar doesn't disappear anymore by default. It might has something to do with the `scrollbars.autoHideSuspend` option, which was introduced in 2.3.0. https://github.com/KingSora/OverlayScrollbars/blob/master/packages/overlayscrollbars/CHANGELOG.md#230
|
||||
example-library@3.4.6 (bug: 3.5.x): Pinned as there is a bug in version 3.5.x that prevents foo from doing bar.
|
||||
|
@ -117,8 +117,8 @@ const ThemedSetRoot = () => {
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
const preview = (window as any).__STORYBOOK_PREVIEW__ as PreviewWeb<ReactRenderer>;
|
||||
const channel = (window as any).__STORYBOOK_ADDONS_CHANNEL__ as Channel;
|
||||
const preview = (window as any).__STORYBOOK_PREVIEW__ as PreviewWeb<ReactRenderer> | undefined;
|
||||
const channel = (window as any).__STORYBOOK_ADDONS_CHANNEL__ as Channel | undefined;
|
||||
export const loaders = [
|
||||
/**
|
||||
* This loader adds a DocsContext to the story, which is required for the most Blocks to work. A
|
||||
@ -133,9 +133,9 @@ export const loaders = [
|
||||
* The DocsContext will then be added via the decorator below.
|
||||
*/
|
||||
async ({ parameters: { relativeCsfPaths, attached = true } }) => {
|
||||
// TODO bring a better way to skip tests when running as part of the vitest plugin instead of __STORYBOOK_URL__
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
if (!relativeCsfPaths || (import.meta as any).env?.__STORYBOOK_URL__) {
|
||||
// __STORYBOOK_PREVIEW__ and __STORYBOOK_ADDONS_CHANNEL__ is set in the PreviewWeb constructor
|
||||
// which isn't loaded in portable stories/vitest
|
||||
if (!relativeCsfPaths || !preview || !channel) {
|
||||
return {};
|
||||
}
|
||||
const csfFiles = await Promise.all(
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/addon-a11y",
|
||||
"version": "8.5.0-alpha.4",
|
||||
"version": "8.5.0-alpha.9",
|
||||
"description": "Test component compliance with web accessibility standards",
|
||||
"keywords": [
|
||||
"a11y",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/addon-actions",
|
||||
"version": "8.5.0-alpha.4",
|
||||
"version": "8.5.0-alpha.9",
|
||||
"description": "Get UI feedback when an action is performed on an interactive element",
|
||||
"keywords": [
|
||||
"storybook",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/addon-backgrounds",
|
||||
"version": "8.5.0-alpha.4",
|
||||
"version": "8.5.0-alpha.9",
|
||||
"description": "Switch backgrounds to view components in different settings",
|
||||
"keywords": [
|
||||
"addon",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/addon-controls",
|
||||
"version": "8.5.0-alpha.4",
|
||||
"version": "8.5.0-alpha.9",
|
||||
"description": "Interact with component inputs dynamically in the Storybook UI",
|
||||
"keywords": [
|
||||
"addon",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/addon-docs",
|
||||
"version": "8.5.0-alpha.4",
|
||||
"version": "8.5.0-alpha.9",
|
||||
"description": "Document component usage and properties in Markdown",
|
||||
"keywords": [
|
||||
"addon",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/addon-essentials",
|
||||
"version": "8.5.0-alpha.4",
|
||||
"version": "8.5.0-alpha.9",
|
||||
"description": "Curated addons to bring out the best of Storybook",
|
||||
"keywords": [
|
||||
"addon",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/addon-mdx-gfm",
|
||||
"version": "8.5.0-alpha.4",
|
||||
"version": "8.5.0-alpha.9",
|
||||
"description": "GitHub Flavored Markdown in Storybook",
|
||||
"keywords": [
|
||||
"addon",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/addon-highlight",
|
||||
"version": "8.5.0-alpha.4",
|
||||
"version": "8.5.0-alpha.9",
|
||||
"description": "Highlight DOM nodes within your stories",
|
||||
"keywords": [
|
||||
"storybook-addons",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/addon-interactions",
|
||||
"version": "8.5.0-alpha.4",
|
||||
"version": "8.5.0-alpha.9",
|
||||
"description": "Automate, test and debug user interactions",
|
||||
"keywords": [
|
||||
"storybook-addons",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/addon-jest",
|
||||
"version": "8.5.0-alpha.4",
|
||||
"version": "8.5.0-alpha.9",
|
||||
"description": "React storybook addon that show component jest report",
|
||||
"keywords": [
|
||||
"addon",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/addon-links",
|
||||
"version": "8.5.0-alpha.4",
|
||||
"version": "8.5.0-alpha.9",
|
||||
"description": "Link stories together to build demos and prototypes with your UI components",
|
||||
"keywords": [
|
||||
"storybook-addons",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/addon-measure",
|
||||
"version": "8.5.0-alpha.4",
|
||||
"version": "8.5.0-alpha.9",
|
||||
"description": "Inspect layouts by visualizing the box model",
|
||||
"keywords": [
|
||||
"storybook-addons",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/addon-onboarding",
|
||||
"version": "8.5.0-alpha.4",
|
||||
"version": "8.5.0-alpha.9",
|
||||
"description": "Storybook Addon Onboarding - Introduces a new onboarding experience",
|
||||
"keywords": [
|
||||
"storybook-addons",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/addon-outline",
|
||||
"version": "8.5.0-alpha.4",
|
||||
"version": "8.5.0-alpha.9",
|
||||
"description": "Outline all elements with CSS to help with layout placement and alignment",
|
||||
"keywords": [
|
||||
"storybook-addons",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/addon-storysource",
|
||||
"version": "8.5.0-alpha.4",
|
||||
"version": "8.5.0-alpha.9",
|
||||
"description": "View a story’s source code to see how it works and paste into your app",
|
||||
"keywords": [
|
||||
"addon",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/experimental-addon-test",
|
||||
"version": "8.5.0-alpha.4",
|
||||
"version": "8.5.0-alpha.9",
|
||||
"description": "Integrate Vitest with Storybook",
|
||||
"keywords": [
|
||||
"storybook-addons",
|
||||
@ -98,6 +98,7 @@
|
||||
"execa": "^8.0.1",
|
||||
"find-up": "^7.0.0",
|
||||
"formik": "^2.2.9",
|
||||
"pathe": "^1.1.2",
|
||||
"picocolors": "^1.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
|
101
code/addons/test/src/components/ContextMenuItem.tsx
Normal file
101
code/addons/test/src/components/ContextMenuItem.tsx
Normal file
@ -0,0 +1,101 @@
|
||||
import React, {
|
||||
type FC,
|
||||
type SyntheticEvent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { Button, type ListItem } from 'storybook/internal/components';
|
||||
import { useStorybookApi } from 'storybook/internal/manager-api';
|
||||
import { useTheme } from 'storybook/internal/theming';
|
||||
import { type API_HashEntry, type Addon_TestProviderState } from 'storybook/internal/types';
|
||||
|
||||
import { PlayHollowIcon, StopAltHollowIcon } from '@storybook/icons';
|
||||
|
||||
import { TEST_PROVIDER_ID } from '../constants';
|
||||
import type { TestResult } from '../node/reporter';
|
||||
import { RelativeTime } from './RelativeTime';
|
||||
|
||||
export const ContextMenuItem: FC<{
|
||||
context: API_HashEntry;
|
||||
state: Addon_TestProviderState<{
|
||||
testResults: TestResult[];
|
||||
}>;
|
||||
ListItem: typeof ListItem;
|
||||
}> = ({ context, state, ListItem }) => {
|
||||
const api = useStorybookApi();
|
||||
const [isDisabled, setDisabled] = useState(false);
|
||||
|
||||
const id = useRef(context.id);
|
||||
id.current = context.id;
|
||||
|
||||
const Icon = state.running ? StopAltHollowIcon : PlayHollowIcon;
|
||||
|
||||
useEffect(() => {
|
||||
setDisabled(false);
|
||||
}, [state.running]);
|
||||
|
||||
const onClick = useCallback(
|
||||
(event: SyntheticEvent) => {
|
||||
setDisabled(true);
|
||||
event.stopPropagation();
|
||||
if (state.running) {
|
||||
api.cancelTestProvider(TEST_PROVIDER_ID);
|
||||
} else {
|
||||
api.runTestProvider(TEST_PROVIDER_ID, { entryId: id.current });
|
||||
}
|
||||
},
|
||||
[api, state.running]
|
||||
);
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const title = state.crashed || state.failed ? 'Component tests failed' : 'Component tests';
|
||||
const errorMessage = state.error?.message;
|
||||
let description: string | React.ReactNode = 'Not run';
|
||||
|
||||
if (state.running) {
|
||||
description = state.progress
|
||||
? `Testing... ${state.progress.numPassedTests}/${state.progress.numTotalTests}`
|
||||
: 'Starting...';
|
||||
} else if (state.failed && !errorMessage) {
|
||||
description = '';
|
||||
} else if (state.crashed || (state.failed && errorMessage)) {
|
||||
description = 'An error occured';
|
||||
} else if (state.progress?.finishedAt) {
|
||||
description = (
|
||||
<RelativeTime
|
||||
timestamp={new Date(state.progress.finishedAt)}
|
||||
testCount={state.progress.numTotalTests}
|
||||
/>
|
||||
);
|
||||
} else if (state.watching) {
|
||||
description = 'Watching for file changes';
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={(event) => {
|
||||
// stopPropagation to prevent the parent from closing the context menu, which is the default behavior onClick
|
||||
event.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<ListItem
|
||||
title={title}
|
||||
center={description}
|
||||
right={
|
||||
<Button
|
||||
onClick={onClick}
|
||||
variant="ghost"
|
||||
padding="small"
|
||||
disabled={state.crashed || isDisabled}
|
||||
>
|
||||
<Icon fill={theme.barTextColor} />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -19,8 +19,8 @@ import { global } from '@storybook/global';
|
||||
import { type Call, CallStates, EVENTS, type LogItem } from '@storybook/instrumenter';
|
||||
import type { API_StatusValue } from '@storybook/types';
|
||||
|
||||
import { InteractionsPanel } from './components/InteractionsPanel';
|
||||
import { ADDON_ID, TEST_PROVIDER_ID } from './constants';
|
||||
import { ADDON_ID, TEST_PROVIDER_ID } from '../constants';
|
||||
import { InteractionsPanel } from './InteractionsPanel';
|
||||
|
||||
interface Interaction extends Call {
|
||||
status: Call['status'];
|
23
code/addons/test/src/components/PanelTitle.tsx
Normal file
23
code/addons/test/src/components/PanelTitle.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Badge, Spaced } from 'storybook/internal/components';
|
||||
import { useAddonState } from 'storybook/internal/manager-api';
|
||||
|
||||
import { ADDON_ID } from '../constants';
|
||||
|
||||
export function PanelTitle() {
|
||||
const [addonState = {}] = useAddonState(ADDON_ID);
|
||||
const { hasException, interactionsCount } = addonState as any;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Spaced col={1}>
|
||||
<span style={{ display: 'inline-block', verticalAlign: 'middle' }}>Component tests</span>
|
||||
{interactionsCount && !hasException ? (
|
||||
<Badge status="neutral">{interactionsCount}</Badge>
|
||||
) : null}
|
||||
{hasException ? <Badge status="negative">{interactionsCount}</Badge> : null}
|
||||
</Spaced>
|
||||
</div>
|
||||
);
|
||||
}
|
24
code/addons/test/src/components/RelativeTime.tsx
Normal file
24
code/addons/test/src/components/RelativeTime.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { getRelativeTimeString } from '../manager';
|
||||
|
||||
export const RelativeTime = ({ timestamp, testCount }: { timestamp: Date; testCount: number }) => {
|
||||
const [relativeTimeString, setRelativeTimeString] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (timestamp) {
|
||||
setRelativeTimeString(getRelativeTimeString(timestamp).replace(/^now$/, 'just now'));
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setRelativeTimeString(getRelativeTimeString(timestamp).replace(/^now$/, 'just now'));
|
||||
}, 10000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, [timestamp]);
|
||||
|
||||
return (
|
||||
relativeTimeString &&
|
||||
`Ran ${testCount} ${testCount === 1 ? 'test' : 'tests'} ${relativeTimeString}`
|
||||
);
|
||||
};
|
@ -1,9 +1,10 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { AddonPanel, Badge, Link as LinkComponent, Spaced } from 'storybook/internal/components';
|
||||
import { AddonPanel, Button, Link as LinkComponent } from 'storybook/internal/components';
|
||||
import { TESTING_MODULE_RUN_ALL_REQUEST } from 'storybook/internal/core-events';
|
||||
import type { Combo } from 'storybook/internal/manager-api';
|
||||
import { Consumer, addons, types, useAddonState } from 'storybook/internal/manager-api';
|
||||
import { Consumer, addons, types } from 'storybook/internal/manager-api';
|
||||
import { styled } from 'storybook/internal/theming';
|
||||
import {
|
||||
type API_StatusObject,
|
||||
type API_StatusValue,
|
||||
@ -11,28 +12,16 @@ import {
|
||||
Addon_TypesEnum,
|
||||
} from 'storybook/internal/types';
|
||||
|
||||
import { Panel } from './Panel';
|
||||
import { EyeIcon, PlayHollowIcon, StopAltHollowIcon } from '@storybook/icons';
|
||||
|
||||
import { ContextMenuItem } from './components/ContextMenuItem';
|
||||
import { GlobalErrorModal } from './components/GlobalErrorModal';
|
||||
import { Panel } from './components/Panel';
|
||||
import { PanelTitle } from './components/PanelTitle';
|
||||
import { RelativeTime } from './components/RelativeTime';
|
||||
import { ADDON_ID, PANEL_ID, TEST_PROVIDER_ID } from './constants';
|
||||
import type { TestResult } from './node/reporter';
|
||||
|
||||
function Title() {
|
||||
const [addonState = {}] = useAddonState(ADDON_ID);
|
||||
const { hasException, interactionsCount } = addonState as any;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Spaced col={1}>
|
||||
<span style={{ display: 'inline-block', verticalAlign: 'middle' }}>Component tests</span>
|
||||
{interactionsCount && !hasException ? (
|
||||
<Badge status="neutral">{interactionsCount}</Badge>
|
||||
) : null}
|
||||
{hasException ? <Badge status="negative">{interactionsCount}</Badge> : null}
|
||||
</Spaced>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const statusMap: Record<any['status'], API_StatusValue> = {
|
||||
failed: 'error',
|
||||
passed: 'success',
|
||||
@ -58,26 +47,27 @@ export function getRelativeTimeString(date: Date): string {
|
||||
return rtf.format(Math.floor(delta / divisor), units[unitIndex]);
|
||||
}
|
||||
|
||||
const RelativeTime = ({ timestamp, testCount }: { timestamp: Date; testCount: number }) => {
|
||||
const [relativeTimeString, setRelativeTimeString] = useState(null);
|
||||
const Info = styled.div({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
marginLeft: 6,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (timestamp) {
|
||||
setRelativeTimeString(getRelativeTimeString(timestamp).replace(/^now$/, 'just now'));
|
||||
const SidebarContextMenuTitle = styled.div<{ crashed?: boolean }>(({ crashed, theme }) => ({
|
||||
fontSize: theme.typography.size.s1,
|
||||
fontWeight: crashed ? 'bold' : 'normal',
|
||||
color: crashed ? theme.color.negativeText : theme.color.defaultText,
|
||||
}));
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setRelativeTimeString(getRelativeTimeString(timestamp).replace(/^now$/, 'just now'));
|
||||
}, 10000);
|
||||
const Description = styled.div(({ theme }) => ({
|
||||
fontSize: theme.typography.size.s1,
|
||||
color: theme.barTextColor,
|
||||
}));
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, [timestamp]);
|
||||
|
||||
return (
|
||||
relativeTimeString &&
|
||||
`Ran ${testCount} ${testCount === 1 ? 'test' : 'tests'} ${relativeTimeString}`
|
||||
);
|
||||
};
|
||||
const Actions = styled.div({
|
||||
display: 'flex',
|
||||
gap: 6,
|
||||
});
|
||||
|
||||
addons.register(ADDON_ID, (api) => {
|
||||
const storybookBuilder = (globalThis as any).STORYBOOK_BUILDER || '';
|
||||
@ -91,25 +81,34 @@ addons.register(ADDON_ID, (api) => {
|
||||
type: Addon_TypesEnum.experimental_TEST_PROVIDER,
|
||||
runnable: true,
|
||||
watchable: true,
|
||||
|
||||
name: 'Component tests',
|
||||
title: ({ crashed, failed }) =>
|
||||
crashed || failed ? 'Component tests failed' : 'Component tests',
|
||||
description: ({ failed, running, watching, progress, crashed, error }) => {
|
||||
|
||||
sidebarContextMenu: ({ context, state }, { ListItem }) => {
|
||||
if (context.type === 'docs') {
|
||||
return null;
|
||||
}
|
||||
if (context.type === 'story' && !context.tags.includes('test')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <ContextMenuItem context={context} state={state} ListItem={ListItem} />;
|
||||
},
|
||||
|
||||
render: (state) => {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
const errorMessage = error?.message;
|
||||
const title = state.crashed || state.failed ? 'Component tests failed' : 'Component tests';
|
||||
const errorMessage = state.error?.message;
|
||||
let description: string | React.ReactNode = 'Not run';
|
||||
|
||||
let message: string | React.ReactNode = 'Not run';
|
||||
|
||||
if (running) {
|
||||
message = progress
|
||||
? `Testing... ${progress.numPassedTests}/${progress.numTotalTests}`
|
||||
if (state.running) {
|
||||
description = state.progress
|
||||
? `Testing... ${state.progress.numPassedTests}/${state.progress.numTotalTests}`
|
||||
: 'Starting...';
|
||||
} else if (failed && !errorMessage) {
|
||||
message = '';
|
||||
} else if (crashed || (failed && errorMessage)) {
|
||||
message = (
|
||||
} else if (state.failed && !errorMessage) {
|
||||
description = '';
|
||||
} else if (state.crashed || (state.failed && errorMessage)) {
|
||||
description = (
|
||||
<>
|
||||
<LinkComponent
|
||||
isButton
|
||||
@ -117,24 +116,70 @@ addons.register(ADDON_ID, (api) => {
|
||||
setIsModalOpen(true);
|
||||
}}
|
||||
>
|
||||
{error?.name || 'View full error'}
|
||||
{state.error?.name || 'View full error'}
|
||||
</LinkComponent>
|
||||
</>
|
||||
);
|
||||
} else if (progress?.finishedAt) {
|
||||
message = (
|
||||
} else if (state.progress?.finishedAt) {
|
||||
description = (
|
||||
<RelativeTime
|
||||
timestamp={new Date(progress.finishedAt)}
|
||||
testCount={progress.numTotalTests}
|
||||
timestamp={new Date(state.progress.finishedAt)}
|
||||
testCount={state.progress.numTotalTests}
|
||||
/>
|
||||
);
|
||||
} else if (watching) {
|
||||
message = 'Watching for file changes';
|
||||
} else if (state.watching) {
|
||||
description = 'Watching for file changes';
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{message}
|
||||
<Info>
|
||||
<SidebarContextMenuTitle crashed={state.crashed} id="testing-module-title">
|
||||
{title}
|
||||
</SidebarContextMenuTitle>
|
||||
<Description id="testing-module-description">{description}</Description>
|
||||
</Info>
|
||||
|
||||
<Actions>
|
||||
{state.watchable && (
|
||||
<Button
|
||||
aria-label={`${state.watching ? 'Disable' : 'Enable'} watch mode for ${state.name}`}
|
||||
variant="ghost"
|
||||
padding="small"
|
||||
active={state.watching}
|
||||
onClick={() => api.setTestProviderWatchMode(state.id, !state.watching)}
|
||||
disabled={state.crashed || state.running}
|
||||
>
|
||||
<EyeIcon />
|
||||
</Button>
|
||||
)}
|
||||
{state.runnable && (
|
||||
<>
|
||||
{state.running && state.cancellable ? (
|
||||
<Button
|
||||
aria-label={`Stop ${state.name}`}
|
||||
variant="ghost"
|
||||
padding="small"
|
||||
onClick={() => api.cancelTestProvider(state.id)}
|
||||
disabled={state.cancelling}
|
||||
>
|
||||
<StopAltHollowIcon />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
aria-label={`Start ${state.name}`}
|
||||
variant="ghost"
|
||||
padding="small"
|
||||
onClick={() => api.runTestProvider(state.id)}
|
||||
disabled={state.crashed || state.running}
|
||||
>
|
||||
<PlayHollowIcon />
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Actions>
|
||||
|
||||
<GlobalErrorModal
|
||||
error={errorMessage}
|
||||
open={isModalOpen}
|
||||
@ -181,20 +226,20 @@ addons.register(ADDON_ID, (api) => {
|
||||
}>);
|
||||
}
|
||||
|
||||
const filter = ({ state }: Combo) => {
|
||||
return {
|
||||
storyId: state.storyId,
|
||||
};
|
||||
};
|
||||
|
||||
addons.add(PANEL_ID, {
|
||||
type: types.PANEL,
|
||||
title: Title,
|
||||
title: () => <PanelTitle />,
|
||||
match: ({ viewMode }) => viewMode === 'story',
|
||||
render: ({ active }) => {
|
||||
const newLocal = useCallback(({ state }: Combo) => {
|
||||
return {
|
||||
storyId: state.storyId,
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AddonPanel active={active}>
|
||||
<Consumer filter={newLocal}>{({ storyId }) => <Panel storyId={storyId} />}</Consumer>
|
||||
<Consumer filter={filter}>{({ storyId }) => <Panel storyId={storyId} />}</Consumer>
|
||||
</AddonPanel>
|
||||
);
|
||||
},
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { type ChildProcess } from 'node:child_process';
|
||||
import { join } from 'node:path';
|
||||
|
||||
import type { Channel } from 'storybook/internal/channels';
|
||||
import {
|
||||
@ -13,6 +12,7 @@ import {
|
||||
|
||||
// eslint-disable-next-line depend/ban-dependencies
|
||||
import { execaNode } from 'execa';
|
||||
import { join } from 'pathe';
|
||||
|
||||
import { TEST_PROVIDER_ID } from '../constants';
|
||||
import { log } from '../logger';
|
||||
|
@ -3,7 +3,7 @@ import { createVitest } from 'vitest/node';
|
||||
|
||||
import { Channel, type ChannelTransport } from '@storybook/core/channels';
|
||||
|
||||
import path from 'path';
|
||||
import path from 'pathe';
|
||||
|
||||
import { TEST_PROVIDER_ID } from '../constants';
|
||||
import { TestManager } from './test-manager';
|
||||
@ -17,6 +17,9 @@ const vitest = vi.hoisted(() => ({
|
||||
cancelCurrentRun: vi.fn(),
|
||||
globTestSpecs: vi.fn(),
|
||||
getModuleProjects: vi.fn(() => []),
|
||||
configOverride: {
|
||||
testNamePattern: undefined,
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('vitest/node', () => ({
|
||||
@ -84,12 +87,10 @@ describe('TestManager', () => {
|
||||
{
|
||||
stories: [],
|
||||
importPath: 'path/to/file',
|
||||
componentPath: 'path/to/component',
|
||||
},
|
||||
{
|
||||
stories: [],
|
||||
importPath: 'path/to/another/file',
|
||||
componentPath: 'path/to/another/component',
|
||||
},
|
||||
],
|
||||
});
|
||||
@ -107,7 +108,6 @@ describe('TestManager', () => {
|
||||
{
|
||||
stories: [],
|
||||
importPath: 'path/to/unknown/file',
|
||||
componentPath: 'path/to/unknown/component',
|
||||
},
|
||||
],
|
||||
});
|
||||
@ -119,7 +119,6 @@ describe('TestManager', () => {
|
||||
{
|
||||
stories: [],
|
||||
importPath: 'path/to/file',
|
||||
componentPath: 'path/to/component',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { existsSync } from 'node:fs';
|
||||
import path, { normalize } from 'node:path';
|
||||
|
||||
import type { TestProject, TestSpecification, Vitest, WorkspaceProject } from 'vitest/node';
|
||||
|
||||
import type { Channel } from 'storybook/internal/channels';
|
||||
import type { TestingModuleRunRequestPayload } from 'storybook/internal/core-events';
|
||||
|
||||
import path, { normalize } from 'pathe';
|
||||
import slash from 'slash';
|
||||
|
||||
import { log } from '../logger';
|
||||
@ -58,6 +58,7 @@ export class VitestManager {
|
||||
if (!this.vitest) {
|
||||
await this.startVitest();
|
||||
}
|
||||
this.resetTestNamePattern();
|
||||
|
||||
const storybookTests = await this.getStorybookTestSpecs();
|
||||
for (const storybookTest of storybookTests) {
|
||||
@ -87,6 +88,7 @@ export class VitestManager {
|
||||
if (!this.vitest) {
|
||||
await this.startVitest();
|
||||
}
|
||||
this.resetTestNamePattern();
|
||||
|
||||
// This list contains all the test files (story files) that need to be run
|
||||
// based on the test files that are passed in the tests array
|
||||
@ -96,6 +98,8 @@ export class VitestManager {
|
||||
|
||||
const storybookTests = await this.getStorybookTestSpecs();
|
||||
|
||||
const filteredStoryNames: string[] = [];
|
||||
|
||||
for (const storybookTest of storybookTests) {
|
||||
const match = testPayload.find((test) => {
|
||||
const absoluteImportPath = path.join(process.cwd(), test.importPath);
|
||||
@ -107,12 +111,29 @@ export class VitestManager {
|
||||
this.updateLastChanged(storybookTest.moduleId);
|
||||
}
|
||||
|
||||
if (match.stories?.length) {
|
||||
filteredStoryNames.push(...match.stories.map((story) => story.name));
|
||||
}
|
||||
testList.push(storybookTest);
|
||||
}
|
||||
}
|
||||
|
||||
await this.cancelCurrentRun();
|
||||
|
||||
if (filteredStoryNames.length > 0) {
|
||||
// temporarily set the test name pattern to only run the selected stories
|
||||
// converting a list of story names to a single regex pattern
|
||||
// ie. ['My Story', 'Other Story'] => /^(My Story|Other Story)$/
|
||||
const testNamePattern = new RegExp(
|
||||
`^(${filteredStoryNames
|
||||
.map((name) => name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
||||
.join('|')})$`
|
||||
);
|
||||
this.vitest!.configOverride.testNamePattern = testNamePattern;
|
||||
}
|
||||
|
||||
await this.vitest!.runFiles(testList, true);
|
||||
this.resetTestNamePattern();
|
||||
}
|
||||
|
||||
async cancelCurrentRun() {
|
||||
@ -173,6 +194,7 @@ export class VitestManager {
|
||||
if (!this.vitest) {
|
||||
return;
|
||||
}
|
||||
this.resetTestNamePattern();
|
||||
|
||||
const globTestFiles = await this.vitest.globTestSpecs();
|
||||
const testGraphs = await Promise.all(
|
||||
@ -219,6 +241,7 @@ export class VitestManager {
|
||||
}
|
||||
|
||||
async setupWatchers() {
|
||||
this.resetTestNamePattern();
|
||||
this.vitest?.server?.watcher.removeAllListeners('change');
|
||||
this.vitest?.server?.watcher.removeAllListeners('add');
|
||||
this.vitest?.server?.watcher.on('change', this.runAffectedTestsAfterChange.bind(this));
|
||||
@ -226,6 +249,12 @@ export class VitestManager {
|
||||
this.registerVitestConfigListener();
|
||||
}
|
||||
|
||||
resetTestNamePattern() {
|
||||
if (this.vitest) {
|
||||
this.vitest.configOverride.testNamePattern = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
isStorybookProject(project: TestProject | WorkspaceProject) {
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
return !!project.config.env?.__STORYBOOK_URL__;
|
||||
|
@ -1,8 +1,6 @@
|
||||
import { existsSync } from 'node:fs';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import { writeFile } from 'node:fs/promises';
|
||||
import { dirname, join, relative } from 'node:path';
|
||||
import * as path from 'node:path';
|
||||
|
||||
import {
|
||||
JsPackageManagerFactory,
|
||||
@ -16,6 +14,7 @@ import { colors, logger } from 'storybook/internal/node-logger';
|
||||
// eslint-disable-next-line depend/ban-dependencies
|
||||
import { execa } from 'execa';
|
||||
import { findUp } from 'find-up';
|
||||
import { dirname, extname, join, relative, resolve } from 'pathe';
|
||||
import picocolors from 'picocolors';
|
||||
import prompts from 'prompts';
|
||||
import { coerce, satisfies } from 'semver';
|
||||
@ -27,7 +26,8 @@ import { printError, printInfo, printSuccess, step } from './postinstall-logger'
|
||||
const ADDON_NAME = '@storybook/experimental-addon-test' as const;
|
||||
const EXTENSIONS = ['.js', '.jsx', '.ts', '.tsx', '.cts', '.mts', '.cjs', '.mjs'] as const;
|
||||
|
||||
const findFile = async (basename: string) => findUp(EXTENSIONS.map((ext) => basename + ext));
|
||||
const findFile = async (basename: string, extraExtensions: string[] = []) =>
|
||||
findUp([...EXTENSIONS, ...extraExtensions].map((ext) => basename + ext));
|
||||
|
||||
export default async function postInstall(options: PostinstallOptions) {
|
||||
printSuccess(
|
||||
@ -244,7 +244,10 @@ export default async function postInstall(options: PostinstallOptions) {
|
||||
args: ['playwright', 'install', 'chromium', '--with-deps'],
|
||||
});
|
||||
|
||||
const vitestSetupFile = path.resolve(options.configDir, 'vitest.setup.ts');
|
||||
const fileExtension =
|
||||
allDeps['typescript'] || (await findFile('tsconfig', ['.json'])) ? 'ts' : 'js';
|
||||
|
||||
const vitestSetupFile = resolve(options.configDir, `vitest.setup.${fileExtension}`);
|
||||
if (existsSync(vitestSetupFile)) {
|
||||
printError(
|
||||
'🚨 Oh no!',
|
||||
@ -264,9 +267,9 @@ export default async function postInstall(options: PostinstallOptions) {
|
||||
logger.plain(`${step} Creating a Vitest setup file for Storybook:`);
|
||||
logger.plain(colors.gray(` ${vitestSetupFile}`));
|
||||
|
||||
const previewExists = EXTENSIONS.map((ext) =>
|
||||
path.resolve(options.configDir, `preview${ext}`)
|
||||
).some((config) => existsSync(config));
|
||||
const previewExists = EXTENSIONS.map((ext) => resolve(options.configDir, `preview${ext}`)).some(
|
||||
(config) => existsSync(config)
|
||||
);
|
||||
|
||||
await writeFile(
|
||||
vitestSetupFile,
|
||||
@ -331,10 +334,10 @@ export default async function postInstall(options: PostinstallOptions) {
|
||||
|
||||
if (rootConfig) {
|
||||
// If there's an existing config, we create a workspace file so we can run Storybook tests alongside.
|
||||
const extname = path.extname(rootConfig);
|
||||
const browserWorkspaceFile = path.resolve(dirname(rootConfig), `vitest.workspace${extname}`);
|
||||
const extension = extname(rootConfig);
|
||||
const browserWorkspaceFile = resolve(dirname(rootConfig), `vitest.workspace${extension}`);
|
||||
// to be set in vitest config
|
||||
const vitestSetupFilePath = path.relative(path.dirname(browserWorkspaceFile), vitestSetupFile);
|
||||
const vitestSetupFilePath = relative(dirname(browserWorkspaceFile), vitestSetupFile);
|
||||
|
||||
logger.line(1);
|
||||
logger.plain(`${step} Creating a Vitest project workspace file:`);
|
||||
@ -373,9 +376,9 @@ export default async function postInstall(options: PostinstallOptions) {
|
||||
);
|
||||
} else {
|
||||
// If there's no existing Vitest/Vite config, we create a new Vitest config file.
|
||||
const newVitestConfigFile = path.resolve('vitest.config.ts');
|
||||
const newVitestConfigFile = resolve(`vitest.config.${fileExtension}`);
|
||||
// to be set in vitest config
|
||||
const vitestSetupFilePath = path.relative(path.dirname(newVitestConfigFile), vitestSetupFile);
|
||||
const vitestSetupFilePath = relative(dirname(newVitestConfigFile), vitestSetupFile);
|
||||
|
||||
logger.line(1);
|
||||
logger.plain(`${step} Creating a Vitest project config file:`);
|
||||
@ -453,6 +456,12 @@ const getVitestPluginInfo = (framework: string) => {
|
||||
frameworkPluginCall = 'storybookVuePlugin()';
|
||||
}
|
||||
|
||||
if (framework === '@storybook/react-native-web-vite') {
|
||||
frameworkPluginImport =
|
||||
"import { storybookReactNativeWeb } from '@storybook/react-native-web-vite/vite-plugin';";
|
||||
frameworkPluginCall = 'storybookReactNativeWeb()';
|
||||
}
|
||||
|
||||
// spaces for file identation
|
||||
frameworkPluginImport = `\n${frameworkPluginImport}`;
|
||||
frameworkPluginDocs = frameworkPluginDocs ? `\n ${frameworkPluginDocs}` : '';
|
||||
@ -491,14 +500,17 @@ async function getStorybookInfo({ configDir, packageManager: pkgMgr }: Postinsta
|
||||
}
|
||||
|
||||
const builderPackageJson = await fs.readFile(
|
||||
`${typeof builder === 'string' ? builder : builder.name}/package.json`,
|
||||
require.resolve(join(typeof builder === 'string' ? builder : builder.name, 'package.json')),
|
||||
'utf8'
|
||||
);
|
||||
const builderPackageName = JSON.parse(builderPackageJson).name;
|
||||
|
||||
let rendererPackageName: string | undefined;
|
||||
if (renderer) {
|
||||
const rendererPackageJson = await fs.readFile(`${renderer}/package.json`, 'utf8');
|
||||
const rendererPackageJson = await fs.readFile(
|
||||
require.resolve(join(renderer, 'package.json')),
|
||||
'utf8'
|
||||
);
|
||||
rendererPackageName = JSON.parse(rendererPackageJson).name;
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { isAbsolute, join } from 'node:path';
|
||||
|
||||
import type { Channel } from 'storybook/internal/channels';
|
||||
import { checkAddonOrder, getFrameworkName, serverRequire } from 'storybook/internal/common';
|
||||
@ -11,6 +10,7 @@ import {
|
||||
import { oneWayHash, telemetry } from 'storybook/internal/telemetry';
|
||||
import type { Options, PresetProperty, StoryId } from 'storybook/internal/types';
|
||||
|
||||
import { isAbsolute, join } from 'pathe';
|
||||
import picocolors from 'picocolors';
|
||||
import { dedent } from 'ts-dedent';
|
||||
|
||||
|
@ -1,6 +1,4 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
import { join, resolve } from 'node:path';
|
||||
|
||||
import type { Plugin } from 'vitest/config';
|
||||
|
||||
import {
|
||||
@ -12,6 +10,8 @@ import { readConfig, vitestTransform } from 'storybook/internal/csf-tools';
|
||||
import { MainFileMissingError } from 'storybook/internal/server-errors';
|
||||
import type { StoriesEntry } from 'storybook/internal/types';
|
||||
|
||||
import { join, resolve } from 'pathe';
|
||||
|
||||
import type { InternalOptions, UserOptions } from './types';
|
||||
|
||||
const defaultOptions: UserOptions = {
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/addon-themes",
|
||||
"version": "8.5.0-alpha.4",
|
||||
"version": "8.5.0-alpha.9",
|
||||
"description": "Switch between multiple themes for you components in Storybook",
|
||||
"keywords": [
|
||||
"css",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/addon-toolbars",
|
||||
"version": "8.5.0-alpha.4",
|
||||
"version": "8.5.0-alpha.9",
|
||||
"description": "Create your own toolbar items that control story rendering",
|
||||
"keywords": [
|
||||
"addon",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/addon-viewport",
|
||||
"version": "8.5.0-alpha.4",
|
||||
"version": "8.5.0-alpha.9",
|
||||
"description": "Build responsive components by adjusting Storybook’s viewport size and orientation",
|
||||
"keywords": [
|
||||
"addon",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/builder-vite",
|
||||
"version": "8.5.0-alpha.4",
|
||||
"version": "8.5.0-alpha.9",
|
||||
"description": "A plugin to run and build Storybooks with Vite",
|
||||
"homepage": "https://github.com/storybookjs/storybook/tree/next/code/builders/builder-vite/#readme",
|
||||
"bugs": {
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/builder-webpack5",
|
||||
"version": "8.5.0-alpha.4",
|
||||
"version": "8.5.0-alpha.9",
|
||||
"description": "Storybook framework-agnostic API",
|
||||
"keywords": [
|
||||
"storybook"
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/core",
|
||||
"version": "8.5.0-alpha.4",
|
||||
"version": "8.5.0-alpha.9",
|
||||
"description": "Storybook framework-agnostic API",
|
||||
"keywords": [
|
||||
"storybook"
|
||||
@ -37,6 +37,16 @@
|
||||
"import": "./dist/client-logger/index.js",
|
||||
"require": "./dist/client-logger/index.cjs"
|
||||
},
|
||||
"./theming": {
|
||||
"types": "./dist/theming/index.d.ts",
|
||||
"import": "./dist/theming/index.js",
|
||||
"require": "./dist/theming/index.cjs"
|
||||
},
|
||||
"./theming/create": {
|
||||
"types": "./dist/theming/create.d.ts",
|
||||
"import": "./dist/theming/create.js",
|
||||
"require": "./dist/theming/create.cjs"
|
||||
},
|
||||
"./core-server": {
|
||||
"types": "./dist/core-server/index.d.ts",
|
||||
"import": "./dist/core-server/index.js",
|
||||
@ -122,16 +132,6 @@
|
||||
"import": "./dist/components/index.js",
|
||||
"require": "./dist/components/index.cjs"
|
||||
},
|
||||
"./theming": {
|
||||
"types": "./dist/theming/index.d.ts",
|
||||
"import": "./dist/theming/index.js",
|
||||
"require": "./dist/theming/index.cjs"
|
||||
},
|
||||
"./theming/create": {
|
||||
"types": "./dist/theming/create.d.ts",
|
||||
"import": "./dist/theming/create.js",
|
||||
"require": "./dist/theming/create.cjs"
|
||||
},
|
||||
"./docs-tools": {
|
||||
"types": "./dist/docs-tools/index.d.ts",
|
||||
"import": "./dist/docs-tools/index.js",
|
||||
@ -183,6 +183,12 @@
|
||||
"client-logger": [
|
||||
"./dist/client-logger/index.d.ts"
|
||||
],
|
||||
"theming": [
|
||||
"./dist/theming/index.d.ts"
|
||||
],
|
||||
"theming/create": [
|
||||
"./dist/theming/create.d.ts"
|
||||
],
|
||||
"core-server": [
|
||||
"./dist/core-server/index.d.ts"
|
||||
],
|
||||
@ -237,12 +243,6 @@
|
||||
"components": [
|
||||
"./dist/components/index.d.ts"
|
||||
],
|
||||
"theming": [
|
||||
"./dist/theming/index.d.ts"
|
||||
],
|
||||
"theming/create": [
|
||||
"./dist/theming/create.d.ts"
|
||||
],
|
||||
"docs-tools": [
|
||||
"./dist/docs-tools/index.d.ts"
|
||||
],
|
||||
|
@ -9,6 +9,9 @@ export const getEntries = (cwd: string) => {
|
||||
define('src/node-logger/index.ts', ['node'], true),
|
||||
define('src/client-logger/index.ts', ['browser', 'node'], true),
|
||||
|
||||
define('src/theming/index.ts', ['browser', 'node'], true, ['react']),
|
||||
define('src/theming/create.ts', ['browser', 'node'], true, ['react']),
|
||||
|
||||
define('src/core-server/index.ts', ['node'], true),
|
||||
define('src/core-server/presets/common-preset.ts', ['node'], false),
|
||||
define('src/core-server/presets/common-manager.ts', ['browser'], false),
|
||||
@ -35,8 +38,6 @@ export const getEntries = (cwd: string) => {
|
||||
['react', 'react-dom'],
|
||||
['prettier'] // the syntax highlighter uses prettier/standalone to format the code
|
||||
),
|
||||
define('src/theming/index.ts', ['browser', 'node'], true, ['react']),
|
||||
define('src/theming/create.ts', ['browser', 'node'], true, ['react']),
|
||||
define('src/docs-tools/index.ts', ['browser', 'node'], true),
|
||||
|
||||
define('src/manager/globals-module-info.ts', ['node'], true),
|
||||
|
@ -3,6 +3,7 @@ import { join, relative } from 'node:path';
|
||||
import { spawn } from '../../../../scripts/prepare/tools';
|
||||
import { limit, picocolors, process } from '../../../../scripts/prepare/tools';
|
||||
import type { getEntries } from '../entries';
|
||||
import { modifyThemeTypes } from './modifyThemeTypes';
|
||||
|
||||
export async function generateTypesFiles(
|
||||
entries: ReturnType<typeof getEntries>,
|
||||
@ -70,6 +71,11 @@ export async function generateTypesFiles(
|
||||
process.exit(dtsProcess.exitCode || 1);
|
||||
} else {
|
||||
console.log('Generated types for', picocolors.cyan(relative(cwd, dtsEntries[index])));
|
||||
|
||||
if (dtsEntries[index].includes('src/theming/index')) {
|
||||
console.log('Modifying theme types');
|
||||
await modifyThemeTypes();
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
|
@ -14,7 +14,7 @@ export async function modifyThemeTypes() {
|
||||
const contents = await readFile(target, 'utf-8');
|
||||
|
||||
const footer = contents.includes('// auto generated file')
|
||||
? `export { StorybookTheme as Theme } from '../src/index';`
|
||||
? `export { StorybookTheme as Theme } from '../../src/theming/index';`
|
||||
: dedent`
|
||||
interface Theme extends StorybookTheme {}
|
||||
export type { Theme };
|
||||
|
@ -66,7 +66,6 @@ async function run() {
|
||||
await generateTypesMapperFiles(entries);
|
||||
await modifyThemeTypes();
|
||||
await generateTypesFiles(entries, isOptimized, cwd);
|
||||
await modifyThemeTypes();
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -130,6 +130,8 @@ export async function detectBuilder(packageManager: JsPackageManager, projectTyp
|
||||
|
||||
// Fallback to Vite or Webpack based on project type
|
||||
switch (projectType) {
|
||||
case ProjectType.REACT_NATIVE_WEB:
|
||||
return CoreBuilder.Vite;
|
||||
case ProjectType.REACT_SCRIPTS:
|
||||
case ProjectType.ANGULAR:
|
||||
case ProjectType.REACT_NATIVE: // technically react native doesn't use webpack, we just want to set something
|
||||
|
@ -157,6 +157,7 @@ export const frameworkToDefaultBuilder: Record<
|
||||
'preact-vite': CoreBuilder.Vite,
|
||||
'preact-webpack5': CoreBuilder.Webpack5,
|
||||
qwik: CoreBuilder.Vite,
|
||||
'react-native-web-vite': CoreBuilder.Vite,
|
||||
'react-vite': CoreBuilder.Vite,
|
||||
'react-webpack5': CoreBuilder.Webpack5,
|
||||
'server-webpack5': CoreBuilder.Webpack5,
|
||||
@ -193,6 +194,13 @@ export async function getVersionSafe(packageManager: JsPackageManager, packageNa
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export const cliStoriesTargetPath = async () => {
|
||||
if (existsSync('./src')) {
|
||||
return './src/stories';
|
||||
}
|
||||
return './stories';
|
||||
};
|
||||
|
||||
export async function copyTemplateFiles({
|
||||
packageManager,
|
||||
renderer,
|
||||
@ -252,14 +260,7 @@ export async function copyTemplateFiles({
|
||||
throw new Error(`Unsupported renderer: ${renderer} (${baseDir})`);
|
||||
};
|
||||
|
||||
const targetPath = async () => {
|
||||
if (existsSync('./src')) {
|
||||
return './src/stories';
|
||||
}
|
||||
return './stories';
|
||||
};
|
||||
|
||||
const destinationPath = destination ?? (await targetPath());
|
||||
const destinationPath = destination ?? (await cliStoriesTargetPath());
|
||||
if (commonAssetsDir) {
|
||||
await cp(commonAssetsDir, destinationPath, {
|
||||
recursive: true,
|
||||
|
@ -47,6 +47,7 @@ export enum ProjectType {
|
||||
REACT = 'REACT',
|
||||
REACT_SCRIPTS = 'REACT_SCRIPTS',
|
||||
REACT_NATIVE = 'REACT_NATIVE',
|
||||
REACT_NATIVE_WEB = 'REACT_NATIVE_WEB',
|
||||
REACT_PROJECT = 'REACT_PROJECT',
|
||||
WEBPACK_REACT = 'WEBPACK_REACT',
|
||||
NEXTJS = 'NEXTJS',
|
||||
|
@ -141,6 +141,7 @@ function hasNPM(cwd?: string) {
|
||||
env: {
|
||||
...process.env,
|
||||
COREPACK_ENABLE_STRICT: '0',
|
||||
COREPACK_ENABLE_AUTO_PIN: '0',
|
||||
},
|
||||
});
|
||||
return npmVersionCommand.status === 0;
|
||||
@ -153,6 +154,7 @@ function hasBun(cwd?: string) {
|
||||
env: {
|
||||
...process.env,
|
||||
COREPACK_ENABLE_STRICT: '0',
|
||||
COREPACK_ENABLE_AUTO_PIN: '0',
|
||||
},
|
||||
});
|
||||
return pnpmVersionCommand.status === 0;
|
||||
@ -165,6 +167,7 @@ function hasPNPM(cwd?: string) {
|
||||
env: {
|
||||
...process.env,
|
||||
COREPACK_ENABLE_STRICT: '0',
|
||||
COREPACK_ENABLE_AUTO_PIN: '0',
|
||||
},
|
||||
});
|
||||
return pnpmVersionCommand.status === 0;
|
||||
@ -177,6 +180,7 @@ function getYarnVersion(cwd?: string): 1 | 2 | undefined {
|
||||
env: {
|
||||
...process.env,
|
||||
COREPACK_ENABLE_STRICT: '0',
|
||||
COREPACK_ENABLE_AUTO_PIN: '0',
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -32,6 +32,7 @@ export const frameworkToRenderer: Record<
|
||||
html: 'html',
|
||||
preact: 'preact',
|
||||
'react-native': 'react-native',
|
||||
'react-native-web-vite': 'react',
|
||||
react: 'react',
|
||||
server: 'server',
|
||||
svelte: 'svelte',
|
||||
|
@ -1,87 +1,88 @@
|
||||
// auto generated file, do not edit
|
||||
export default {
|
||||
'@storybook/addon-a11y': '8.5.0-alpha.4',
|
||||
'@storybook/addon-actions': '8.5.0-alpha.4',
|
||||
'@storybook/addon-backgrounds': '8.5.0-alpha.4',
|
||||
'@storybook/addon-controls': '8.5.0-alpha.4',
|
||||
'@storybook/addon-docs': '8.5.0-alpha.4',
|
||||
'@storybook/addon-essentials': '8.5.0-alpha.4',
|
||||
'@storybook/addon-mdx-gfm': '8.5.0-alpha.4',
|
||||
'@storybook/addon-highlight': '8.5.0-alpha.4',
|
||||
'@storybook/addon-interactions': '8.5.0-alpha.4',
|
||||
'@storybook/addon-jest': '8.5.0-alpha.4',
|
||||
'@storybook/addon-links': '8.5.0-alpha.4',
|
||||
'@storybook/addon-measure': '8.5.0-alpha.4',
|
||||
'@storybook/addon-onboarding': '8.5.0-alpha.4',
|
||||
'@storybook/addon-outline': '8.5.0-alpha.4',
|
||||
'@storybook/addon-storysource': '8.5.0-alpha.4',
|
||||
'@storybook/experimental-addon-test': '8.5.0-alpha.4',
|
||||
'@storybook/addon-themes': '8.5.0-alpha.4',
|
||||
'@storybook/addon-toolbars': '8.5.0-alpha.4',
|
||||
'@storybook/addon-viewport': '8.5.0-alpha.4',
|
||||
'@storybook/builder-vite': '8.5.0-alpha.4',
|
||||
'@storybook/builder-webpack5': '8.5.0-alpha.4',
|
||||
'@storybook/core': '8.5.0-alpha.4',
|
||||
'@storybook/builder-manager': '8.5.0-alpha.4',
|
||||
'@storybook/channels': '8.5.0-alpha.4',
|
||||
'@storybook/client-logger': '8.5.0-alpha.4',
|
||||
'@storybook/components': '8.5.0-alpha.4',
|
||||
'@storybook/core-common': '8.5.0-alpha.4',
|
||||
'@storybook/core-events': '8.5.0-alpha.4',
|
||||
'@storybook/core-server': '8.5.0-alpha.4',
|
||||
'@storybook/csf-tools': '8.5.0-alpha.4',
|
||||
'@storybook/docs-tools': '8.5.0-alpha.4',
|
||||
'@storybook/manager': '8.5.0-alpha.4',
|
||||
'@storybook/manager-api': '8.5.0-alpha.4',
|
||||
'@storybook/node-logger': '8.5.0-alpha.4',
|
||||
'@storybook/preview': '8.5.0-alpha.4',
|
||||
'@storybook/preview-api': '8.5.0-alpha.4',
|
||||
'@storybook/router': '8.5.0-alpha.4',
|
||||
'@storybook/telemetry': '8.5.0-alpha.4',
|
||||
'@storybook/theming': '8.5.0-alpha.4',
|
||||
'@storybook/types': '8.5.0-alpha.4',
|
||||
'@storybook/angular': '8.5.0-alpha.4',
|
||||
'@storybook/ember': '8.5.0-alpha.4',
|
||||
'@storybook/experimental-nextjs-vite': '8.5.0-alpha.4',
|
||||
'@storybook/html-vite': '8.5.0-alpha.4',
|
||||
'@storybook/html-webpack5': '8.5.0-alpha.4',
|
||||
'@storybook/nextjs': '8.5.0-alpha.4',
|
||||
'@storybook/preact-vite': '8.5.0-alpha.4',
|
||||
'@storybook/preact-webpack5': '8.5.0-alpha.4',
|
||||
'@storybook/react-vite': '8.5.0-alpha.4',
|
||||
'@storybook/react-webpack5': '8.5.0-alpha.4',
|
||||
'@storybook/server-webpack5': '8.5.0-alpha.4',
|
||||
'@storybook/svelte-vite': '8.5.0-alpha.4',
|
||||
'@storybook/svelte-webpack5': '8.5.0-alpha.4',
|
||||
'@storybook/sveltekit': '8.5.0-alpha.4',
|
||||
'@storybook/vue3-vite': '8.5.0-alpha.4',
|
||||
'@storybook/vue3-webpack5': '8.5.0-alpha.4',
|
||||
'@storybook/web-components-vite': '8.5.0-alpha.4',
|
||||
'@storybook/web-components-webpack5': '8.5.0-alpha.4',
|
||||
'@storybook/blocks': '8.5.0-alpha.4',
|
||||
storybook: '8.5.0-alpha.4',
|
||||
sb: '8.5.0-alpha.4',
|
||||
'@storybook/cli': '8.5.0-alpha.4',
|
||||
'@storybook/codemod': '8.5.0-alpha.4',
|
||||
'@storybook/core-webpack': '8.5.0-alpha.4',
|
||||
'create-storybook': '8.5.0-alpha.4',
|
||||
'@storybook/csf-plugin': '8.5.0-alpha.4',
|
||||
'@storybook/instrumenter': '8.5.0-alpha.4',
|
||||
'@storybook/react-dom-shim': '8.5.0-alpha.4',
|
||||
'@storybook/source-loader': '8.5.0-alpha.4',
|
||||
'@storybook/test': '8.5.0-alpha.4',
|
||||
'@storybook/preset-create-react-app': '8.5.0-alpha.4',
|
||||
'@storybook/preset-html-webpack': '8.5.0-alpha.4',
|
||||
'@storybook/preset-preact-webpack': '8.5.0-alpha.4',
|
||||
'@storybook/preset-react-webpack': '8.5.0-alpha.4',
|
||||
'@storybook/preset-server-webpack': '8.5.0-alpha.4',
|
||||
'@storybook/preset-svelte-webpack': '8.5.0-alpha.4',
|
||||
'@storybook/preset-vue3-webpack': '8.5.0-alpha.4',
|
||||
'@storybook/html': '8.5.0-alpha.4',
|
||||
'@storybook/preact': '8.5.0-alpha.4',
|
||||
'@storybook/react': '8.5.0-alpha.4',
|
||||
'@storybook/server': '8.5.0-alpha.4',
|
||||
'@storybook/svelte': '8.5.0-alpha.4',
|
||||
'@storybook/vue3': '8.5.0-alpha.4',
|
||||
'@storybook/web-components': '8.5.0-alpha.4',
|
||||
'@storybook/addon-a11y': '8.5.0-alpha.9',
|
||||
'@storybook/addon-actions': '8.5.0-alpha.9',
|
||||
'@storybook/addon-backgrounds': '8.5.0-alpha.9',
|
||||
'@storybook/addon-controls': '8.5.0-alpha.9',
|
||||
'@storybook/addon-docs': '8.5.0-alpha.9',
|
||||
'@storybook/addon-essentials': '8.5.0-alpha.9',
|
||||
'@storybook/addon-mdx-gfm': '8.5.0-alpha.9',
|
||||
'@storybook/addon-highlight': '8.5.0-alpha.9',
|
||||
'@storybook/addon-interactions': '8.5.0-alpha.9',
|
||||
'@storybook/addon-jest': '8.5.0-alpha.9',
|
||||
'@storybook/addon-links': '8.5.0-alpha.9',
|
||||
'@storybook/addon-measure': '8.5.0-alpha.9',
|
||||
'@storybook/addon-onboarding': '8.5.0-alpha.9',
|
||||
'@storybook/addon-outline': '8.5.0-alpha.9',
|
||||
'@storybook/addon-storysource': '8.5.0-alpha.9',
|
||||
'@storybook/experimental-addon-test': '8.5.0-alpha.9',
|
||||
'@storybook/addon-themes': '8.5.0-alpha.9',
|
||||
'@storybook/addon-toolbars': '8.5.0-alpha.9',
|
||||
'@storybook/addon-viewport': '8.5.0-alpha.9',
|
||||
'@storybook/builder-vite': '8.5.0-alpha.9',
|
||||
'@storybook/builder-webpack5': '8.5.0-alpha.9',
|
||||
'@storybook/core': '8.5.0-alpha.9',
|
||||
'@storybook/builder-manager': '8.5.0-alpha.9',
|
||||
'@storybook/channels': '8.5.0-alpha.9',
|
||||
'@storybook/client-logger': '8.5.0-alpha.9',
|
||||
'@storybook/components': '8.5.0-alpha.9',
|
||||
'@storybook/core-common': '8.5.0-alpha.9',
|
||||
'@storybook/core-events': '8.5.0-alpha.9',
|
||||
'@storybook/core-server': '8.5.0-alpha.9',
|
||||
'@storybook/csf-tools': '8.5.0-alpha.9',
|
||||
'@storybook/docs-tools': '8.5.0-alpha.9',
|
||||
'@storybook/manager': '8.5.0-alpha.9',
|
||||
'@storybook/manager-api': '8.5.0-alpha.9',
|
||||
'@storybook/node-logger': '8.5.0-alpha.9',
|
||||
'@storybook/preview': '8.5.0-alpha.9',
|
||||
'@storybook/preview-api': '8.5.0-alpha.9',
|
||||
'@storybook/router': '8.5.0-alpha.9',
|
||||
'@storybook/telemetry': '8.5.0-alpha.9',
|
||||
'@storybook/theming': '8.5.0-alpha.9',
|
||||
'@storybook/types': '8.5.0-alpha.9',
|
||||
'@storybook/angular': '8.5.0-alpha.9',
|
||||
'@storybook/ember': '8.5.0-alpha.9',
|
||||
'@storybook/experimental-nextjs-vite': '8.5.0-alpha.9',
|
||||
'@storybook/html-vite': '8.5.0-alpha.9',
|
||||
'@storybook/html-webpack5': '8.5.0-alpha.9',
|
||||
'@storybook/nextjs': '8.5.0-alpha.9',
|
||||
'@storybook/preact-vite': '8.5.0-alpha.9',
|
||||
'@storybook/preact-webpack5': '8.5.0-alpha.9',
|
||||
'@storybook/react-native-web-vite': '8.5.0-alpha.9',
|
||||
'@storybook/react-vite': '8.5.0-alpha.9',
|
||||
'@storybook/react-webpack5': '8.5.0-alpha.9',
|
||||
'@storybook/server-webpack5': '8.5.0-alpha.9',
|
||||
'@storybook/svelte-vite': '8.5.0-alpha.9',
|
||||
'@storybook/svelte-webpack5': '8.5.0-alpha.9',
|
||||
'@storybook/sveltekit': '8.5.0-alpha.9',
|
||||
'@storybook/vue3-vite': '8.5.0-alpha.9',
|
||||
'@storybook/vue3-webpack5': '8.5.0-alpha.9',
|
||||
'@storybook/web-components-vite': '8.5.0-alpha.9',
|
||||
'@storybook/web-components-webpack5': '8.5.0-alpha.9',
|
||||
'@storybook/blocks': '8.5.0-alpha.9',
|
||||
storybook: '8.5.0-alpha.9',
|
||||
sb: '8.5.0-alpha.9',
|
||||
'@storybook/cli': '8.5.0-alpha.9',
|
||||
'@storybook/codemod': '8.5.0-alpha.9',
|
||||
'@storybook/core-webpack': '8.5.0-alpha.9',
|
||||
'create-storybook': '8.5.0-alpha.9',
|
||||
'@storybook/csf-plugin': '8.5.0-alpha.9',
|
||||
'@storybook/instrumenter': '8.5.0-alpha.9',
|
||||
'@storybook/react-dom-shim': '8.5.0-alpha.9',
|
||||
'@storybook/source-loader': '8.5.0-alpha.9',
|
||||
'@storybook/test': '8.5.0-alpha.9',
|
||||
'@storybook/preset-create-react-app': '8.5.0-alpha.9',
|
||||
'@storybook/preset-html-webpack': '8.5.0-alpha.9',
|
||||
'@storybook/preset-preact-webpack': '8.5.0-alpha.9',
|
||||
'@storybook/preset-react-webpack': '8.5.0-alpha.9',
|
||||
'@storybook/preset-server-webpack': '8.5.0-alpha.9',
|
||||
'@storybook/preset-svelte-webpack': '8.5.0-alpha.9',
|
||||
'@storybook/preset-vue3-webpack': '8.5.0-alpha.9',
|
||||
'@storybook/html': '8.5.0-alpha.9',
|
||||
'@storybook/preact': '8.5.0-alpha.9',
|
||||
'@storybook/react': '8.5.0-alpha.9',
|
||||
'@storybook/server': '8.5.0-alpha.9',
|
||||
'@storybook/svelte': '8.5.0-alpha.9',
|
||||
'@storybook/vue3': '8.5.0-alpha.9',
|
||||
'@storybook/web-components': '8.5.0-alpha.9',
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
import type { ComponentProps, SyntheticEvent } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import type { ComponentProps, ReactNode, SyntheticEvent } from 'react';
|
||||
import React, { Fragment, useCallback } from 'react';
|
||||
|
||||
import { styled } from '@storybook/core/theming';
|
||||
|
||||
@ -15,7 +15,8 @@ const List = styled.div(
|
||||
},
|
||||
({ theme }) => ({
|
||||
borderRadius: theme.appBorderRadius + 2,
|
||||
})
|
||||
}),
|
||||
({ theme }) => (theme.base === 'dark' ? { background: theme.background.content } : {})
|
||||
);
|
||||
|
||||
const Group = styled.div(({ theme }) => ({
|
||||
@ -25,7 +26,7 @@ const Group = styled.div(({ theme }) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
export interface Link extends Omit<ListItemProps, 'onClick'> {
|
||||
export interface NormalLink extends Omit<ListItemProps, 'onClick'> {
|
||||
id: string;
|
||||
onClick?: (
|
||||
event: SyntheticEvent,
|
||||
@ -33,7 +34,18 @@ export interface Link extends Omit<ListItemProps, 'onClick'> {
|
||||
) => void;
|
||||
}
|
||||
|
||||
interface ItemProps extends Link {
|
||||
export type Link = CustomLink | NormalLink;
|
||||
|
||||
/**
|
||||
* This is a custom link that can be used in the `TooltipLinkList` component. It allows for custom
|
||||
* content to be rendered in the list; it does not have to be a link.
|
||||
*/
|
||||
interface CustomLink {
|
||||
id: string;
|
||||
content: ReactNode;
|
||||
}
|
||||
|
||||
interface ItemProps extends NormalLink {
|
||||
isIndented?: boolean;
|
||||
}
|
||||
|
||||
@ -55,7 +67,7 @@ export interface TooltipLinkListProps extends ComponentProps<typeof List> {
|
||||
|
||||
export const TooltipLinkList = ({ links, LinkWrapper, ...props }: TooltipLinkListProps) => {
|
||||
const groups = Array.isArray(links[0]) ? (links as Link[][]) : [links as Link[]];
|
||||
const isIndented = groups.some((group) => group.some((link) => link.icon));
|
||||
const isIndented = groups.some((group) => group.some((link) => 'icon' in link && link.icon));
|
||||
return (
|
||||
<List {...props}>
|
||||
{groups
|
||||
@ -63,9 +75,14 @@ export const TooltipLinkList = ({ links, LinkWrapper, ...props }: TooltipLinkLis
|
||||
.map((group, index) => {
|
||||
return (
|
||||
<Group key={group.map((link) => link.id).join(`~${index}~`)}>
|
||||
{group.map((link) => (
|
||||
<Item key={link.id} isIndented={isIndented} LinkWrapper={LinkWrapper} {...link} />
|
||||
))}
|
||||
{group.map((link) => {
|
||||
if ('content' in link) {
|
||||
return <Fragment key={link.id}>{link.content}</Fragment>;
|
||||
}
|
||||
return (
|
||||
<Item key={link.id} isIndented={isIndented} LinkWrapper={LinkWrapper} {...link} />
|
||||
);
|
||||
})}
|
||||
</Group>
|
||||
);
|
||||
})}
|
||||
|
@ -121,7 +121,7 @@ const WithTooltipPure = ({
|
||||
}
|
||||
);
|
||||
|
||||
const tooltipComponent = (
|
||||
const tooltipComponent = isVisible ? (
|
||||
<Tooltip
|
||||
placement={state?.placement}
|
||||
ref={setTooltipRef}
|
||||
@ -133,7 +133,7 @@ const WithTooltipPure = ({
|
||||
{/* @ts-expect-error (non strict) */}
|
||||
{typeof tooltip === 'function' ? tooltip({ onHide: () => onVisibleChange(false) }) : tooltip}
|
||||
</Tooltip>
|
||||
);
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -8,17 +8,17 @@ export type TestProviderState = Addon_TestProviderState;
|
||||
|
||||
export type TestProviders = Record<TestProviderId, TestProviderConfig & TestProviderState>;
|
||||
|
||||
export type TestingModuleRunRequestStories = {
|
||||
id: string;
|
||||
name: string;
|
||||
export type TestingModuleRunRequestStory = {
|
||||
id: string; // button--primary
|
||||
name: string; // Primary
|
||||
};
|
||||
|
||||
export type TestingModuleRunRequestPayload = {
|
||||
providerId: TestProviderId;
|
||||
payload: {
|
||||
stories: TestingModuleRunRequestStories[];
|
||||
importPath: string;
|
||||
componentPath: string;
|
||||
importPath: string; // ./.../button.stories.tsx
|
||||
stories?: TestingModuleRunRequestStory[];
|
||||
componentPath?: string; // ./.../button.tsx
|
||||
}[];
|
||||
};
|
||||
|
||||
|
@ -4,8 +4,13 @@ import { parse, stringify } from 'telejson';
|
||||
// setting up the store, overriding set and get to use telejson
|
||||
export default (_: any) => {
|
||||
_.fn('set', function (key: string, data: object) {
|
||||
// @ts-expect-error('this' implicitly has type 'any')
|
||||
return _.set(this._area, this._in(key), stringify(data, { maxDepth: 50 }));
|
||||
return _.set(
|
||||
// @ts-expect-error('this' implicitly has type 'any')
|
||||
this._area,
|
||||
// @ts-expect-error('this' implicitly has type 'any')
|
||||
this._in(key),
|
||||
stringify(data, { maxDepth: 50, allowFunction: false })
|
||||
);
|
||||
});
|
||||
_.fn('get', function (key: string, alt: string) {
|
||||
// @ts-expect-error('this' implicitly has type 'any')
|
||||
|
@ -30,6 +30,7 @@ export interface SubAPI {
|
||||
| Addon_Types
|
||||
| Addon_TypesEnum.experimental_PAGE
|
||||
| Addon_TypesEnum.experimental_SIDEBAR_BOTTOM
|
||||
| Addon_TypesEnum.experimental_TEST_PROVIDER
|
||||
| Addon_TypesEnum.experimental_SIDEBAR_TOP = Addon_Types,
|
||||
>(
|
||||
type: T
|
||||
|
186
code/core/src/manager-api/modules/experimental_testmodule.ts
Normal file
186
code/core/src/manager-api/modules/experimental_testmodule.ts
Normal file
@ -0,0 +1,186 @@
|
||||
import { type API_StoryEntry, Addon_TypesEnum, type StoryId } from '@storybook/core/types';
|
||||
|
||||
import {
|
||||
TESTING_MODULE_CANCEL_TEST_RUN_REQUEST,
|
||||
TESTING_MODULE_RUN_ALL_REQUEST,
|
||||
TESTING_MODULE_RUN_REQUEST,
|
||||
TESTING_MODULE_WATCH_MODE_REQUEST,
|
||||
type TestProviderId,
|
||||
type TestProviderState,
|
||||
type TestProviders,
|
||||
type TestingModuleRunAllRequestPayload,
|
||||
type TestingModuleRunRequestPayload,
|
||||
} from '@storybook/core/core-events';
|
||||
|
||||
import invariant from 'tiny-invariant';
|
||||
|
||||
import type { ModuleFn } from '../lib/types';
|
||||
|
||||
export type SubState = {
|
||||
testProviders: TestProviders;
|
||||
};
|
||||
|
||||
const initialTestProviderState: TestProviderState = {
|
||||
details: {} as { [key: string]: any },
|
||||
cancellable: false,
|
||||
cancelling: false,
|
||||
running: false,
|
||||
watching: false,
|
||||
failed: false,
|
||||
crashed: false,
|
||||
};
|
||||
|
||||
interface RunOptions {
|
||||
entryId?: StoryId;
|
||||
}
|
||||
|
||||
export type SubAPI = {
|
||||
getTestProviderState(id: string): TestProviderState | undefined;
|
||||
updateTestProviderState(id: TestProviderId, update: Partial<TestProviderState>): void;
|
||||
clearTestProviderState(id: TestProviderId): void;
|
||||
runTestProvider(id: TestProviderId, options?: RunOptions): () => void;
|
||||
setTestProviderWatchMode(id: TestProviderId, watchMode: boolean): void;
|
||||
cancelTestProvider(id: TestProviderId): void;
|
||||
};
|
||||
|
||||
export const init: ModuleFn<SubAPI, SubState> = ({ store, fullAPI }) => {
|
||||
const state: SubState = {
|
||||
testProviders: store.getState().testProviders || {},
|
||||
};
|
||||
|
||||
const api: SubAPI = {
|
||||
getTestProviderState(id) {
|
||||
const { testProviders } = store.getState();
|
||||
|
||||
return testProviders?.[id];
|
||||
},
|
||||
updateTestProviderState(id, update) {
|
||||
return store.setState(
|
||||
({ testProviders }) => {
|
||||
return { testProviders: { ...testProviders, [id]: { ...testProviders[id], ...update } } };
|
||||
},
|
||||
{ persistence: 'session' }
|
||||
);
|
||||
},
|
||||
clearTestProviderState(id) {
|
||||
const update = {
|
||||
cancelling: false,
|
||||
running: true,
|
||||
failed: false,
|
||||
crashed: false,
|
||||
progress: undefined,
|
||||
};
|
||||
return store.setState(
|
||||
({ testProviders }) => {
|
||||
return { testProviders: { ...testProviders, [id]: { ...testProviders[id], ...update } } };
|
||||
},
|
||||
{ persistence: 'session' }
|
||||
);
|
||||
},
|
||||
runTestProvider(id, options) {
|
||||
if (!options?.entryId) {
|
||||
const payload: TestingModuleRunAllRequestPayload = { providerId: id };
|
||||
fullAPI.emit(TESTING_MODULE_RUN_ALL_REQUEST, payload);
|
||||
return () => api.cancelTestProvider(id);
|
||||
}
|
||||
|
||||
const index = store.getState().index;
|
||||
invariant(index, 'The index is currently unavailable');
|
||||
|
||||
const entry = index[options.entryId];
|
||||
|
||||
invariant(entry, `No entry found in the index for id '${options.entryId}'`);
|
||||
|
||||
if (entry.type === 'story') {
|
||||
const payload: TestingModuleRunRequestPayload = {
|
||||
providerId: id,
|
||||
payload: [
|
||||
{
|
||||
importPath: entry.importPath,
|
||||
stories: [
|
||||
{
|
||||
id: entry.id,
|
||||
name: entry.name,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
fullAPI.emit(TESTING_MODULE_RUN_REQUEST, payload);
|
||||
return () => api.cancelTestProvider(id);
|
||||
}
|
||||
|
||||
const payloads = new Set<TestingModuleRunRequestPayload['payload'][0]>();
|
||||
|
||||
const findComponents = (entryId: StoryId) => {
|
||||
const foundEntry = index[entryId];
|
||||
switch (foundEntry.type) {
|
||||
case 'component':
|
||||
const firstStoryId = foundEntry.children.find(
|
||||
(childId) => index[childId].type === 'story'
|
||||
);
|
||||
if (!firstStoryId) {
|
||||
// happens when there are only docs in the component
|
||||
return;
|
||||
}
|
||||
payloads.add({ importPath: (index[firstStoryId] as API_StoryEntry).importPath });
|
||||
return;
|
||||
case 'story': {
|
||||
// this shouldn't happen because we don't visit components' children.
|
||||
// so we never get to a story directly.
|
||||
payloads.add({
|
||||
importPath: foundEntry.importPath,
|
||||
stories: [
|
||||
{
|
||||
id: foundEntry.id,
|
||||
name: foundEntry.name,
|
||||
},
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
case 'docs': {
|
||||
return;
|
||||
}
|
||||
default:
|
||||
foundEntry.children.forEach(findComponents);
|
||||
}
|
||||
};
|
||||
findComponents(options.entryId);
|
||||
|
||||
const payload: TestingModuleRunRequestPayload = {
|
||||
providerId: id,
|
||||
payload: Array.from(payloads),
|
||||
};
|
||||
fullAPI.emit(TESTING_MODULE_RUN_REQUEST, payload);
|
||||
|
||||
return () => api.cancelTestProvider(id);
|
||||
},
|
||||
setTestProviderWatchMode(id, watchMode) {
|
||||
api.updateTestProviderState(id, { watching: watchMode });
|
||||
fullAPI.emit(TESTING_MODULE_WATCH_MODE_REQUEST, { providerId: id, watchMode });
|
||||
},
|
||||
cancelTestProvider(id) {
|
||||
api.updateTestProviderState(id, { cancelling: true });
|
||||
fullAPI.emit(TESTING_MODULE_CANCEL_TEST_RUN_REQUEST, { providerId: id });
|
||||
},
|
||||
};
|
||||
|
||||
const initModule = async () => {
|
||||
const initialState: TestProviders = Object.fromEntries(
|
||||
Object.entries(fullAPI.getElements(Addon_TypesEnum.experimental_TEST_PROVIDER)).map(
|
||||
([id, config]) => [
|
||||
id,
|
||||
{
|
||||
...config,
|
||||
...initialTestProviderState,
|
||||
...(state?.testProviders?.[id] || {}),
|
||||
} as TestProviders[0],
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
store.setState({ testProviders: initialState }, { persistence: 'session' });
|
||||
};
|
||||
return { init: initModule, state, api };
|
||||
};
|
@ -50,6 +50,7 @@ import { noArrayMerge } from './lib/merge';
|
||||
import type { ModuleFn } from './lib/types';
|
||||
import * as addons from './modules/addons';
|
||||
import * as channel from './modules/channel';
|
||||
import * as testProviders from './modules/experimental_testmodule';
|
||||
import * as globals from './modules/globals';
|
||||
import * as layout from './modules/layout';
|
||||
import * as notifications from './modules/notifications';
|
||||
@ -79,6 +80,7 @@ export type State = layout.SubState &
|
||||
stories.SubState &
|
||||
refs.SubState &
|
||||
notifications.SubState &
|
||||
testProviders.SubState &
|
||||
version.SubState &
|
||||
url.SubState &
|
||||
shortcuts.SubState &
|
||||
@ -98,6 +100,7 @@ export type API = addons.SubAPI &
|
||||
globals.SubAPI &
|
||||
layout.SubAPI &
|
||||
notifications.SubAPI &
|
||||
testProviders.SubAPI &
|
||||
shortcuts.SubAPI &
|
||||
settings.SubAPI &
|
||||
version.SubAPI &
|
||||
@ -178,6 +181,7 @@ class ManagerProvider extends Component<ManagerProviderProps, State> {
|
||||
addons,
|
||||
layout,
|
||||
notifications,
|
||||
testProviders,
|
||||
settings,
|
||||
shortcuts,
|
||||
stories,
|
||||
|
@ -1 +1 @@
|
||||
export const version = '8.5.0-alpha.4';
|
||||
export const version = '8.5.0-alpha.9';
|
||||
|
121
code/core/src/manager/components/sidebar/ContextMenu.tsx
Normal file
121
code/core/src/manager/components/sidebar/ContextMenu.tsx
Normal file
@ -0,0 +1,121 @@
|
||||
import type { ComponentProps, FC, SyntheticEvent } from 'react';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
|
||||
import { TooltipLinkList, WithTooltip } from '@storybook/core/components';
|
||||
import { type API_HashEntry, Addon_TypesEnum } from '@storybook/core/types';
|
||||
import { EllipsisIcon } from '@storybook/icons';
|
||||
|
||||
import { type TestProviders } from '@storybook/core/core-events';
|
||||
import { useStorybookState } from '@storybook/core/manager-api';
|
||||
import type { API } from '@storybook/core/manager-api';
|
||||
|
||||
import type { Link } from '../../../components/components/tooltip/TooltipLinkList';
|
||||
import { StatusButton } from './StatusButton';
|
||||
import type { ExcludesNull } from './Tree';
|
||||
import { ContextMenu } from './Tree';
|
||||
|
||||
export const useContextMenu = (context: API_HashEntry, links: Link[], api: API) => {
|
||||
const [hoverCount, setHoverCount] = useState(0);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const handlers = useMemo(() => {
|
||||
return {
|
||||
onMouseEnter: () => {
|
||||
setHoverCount((c) => c + 1);
|
||||
},
|
||||
onOpen: (event: SyntheticEvent) => {
|
||||
event.stopPropagation();
|
||||
setIsOpen(true);
|
||||
},
|
||||
onClose: () => {
|
||||
setIsOpen(false);
|
||||
},
|
||||
};
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Calculate the providerLinks whenever the user mouses over the container. We use an incrementer,
|
||||
* instead of a simple boolean to ensure that the links are recalculated
|
||||
*/
|
||||
const providerLinks = useMemo(() => {
|
||||
const testProviders = api.getElements(
|
||||
Addon_TypesEnum.experimental_TEST_PROVIDER
|
||||
) as any as TestProviders;
|
||||
|
||||
if (hoverCount) {
|
||||
return generateTestProviderLinks(testProviders, context);
|
||||
}
|
||||
return [];
|
||||
}, [api, context, hoverCount]);
|
||||
|
||||
const isRendered = providerLinks.length > 0 || links.length > 0;
|
||||
|
||||
return useMemo(() => {
|
||||
return {
|
||||
onMouseEnter: handlers.onMouseEnter,
|
||||
node: isRendered ? (
|
||||
<WithTooltip
|
||||
data-displayed={isOpen ? 'on' : 'off'}
|
||||
closeOnOutsideClick
|
||||
placement="bottom-end"
|
||||
data-testid="context-menu"
|
||||
onVisibleChange={(visible) => {
|
||||
if (!visible) {
|
||||
handlers.onClose();
|
||||
} else {
|
||||
setIsOpen(true);
|
||||
}
|
||||
}}
|
||||
tooltip={({ onHide }) => (
|
||||
<LiveContextMenu context={context} links={links} onClick={onHide} />
|
||||
)}
|
||||
>
|
||||
<StatusButton type="button" status={'pending'}>
|
||||
<EllipsisIcon />
|
||||
</StatusButton>
|
||||
</WithTooltip>
|
||||
) : null,
|
||||
};
|
||||
}, [context, handlers, isOpen, isRendered, links]);
|
||||
};
|
||||
|
||||
/**
|
||||
* This component re-subscribes to storybook's core state, hence the Live prefix. It is used to
|
||||
* render the context menu for the sidebar. it self is a tooltip link list that renders the links
|
||||
* provided to it. In addition to the links, it also renders the test providers.
|
||||
*/
|
||||
const LiveContextMenu: FC<{ context: API_HashEntry } & ComponentProps<typeof TooltipLinkList>> = ({
|
||||
context,
|
||||
links,
|
||||
...rest
|
||||
}) => {
|
||||
const { testProviders } = useStorybookState();
|
||||
const providerLinks: Link[] = generateTestProviderLinks(testProviders, context);
|
||||
const groups = Array.isArray(links[0]) ? (links as Link[][]) : [links as Link[]];
|
||||
const all = groups.concat([providerLinks]);
|
||||
|
||||
return <TooltipLinkList {...rest} links={all} />;
|
||||
};
|
||||
|
||||
export function generateTestProviderLinks(
|
||||
testProviders: TestProviders,
|
||||
context: API_HashEntry
|
||||
): Link[] {
|
||||
return Object.entries(testProviders)
|
||||
.map(([testProviderId, state]) => {
|
||||
if (!state) {
|
||||
return null;
|
||||
}
|
||||
const content = state.sidebarContextMenu?.({ context, state }, ContextMenu);
|
||||
|
||||
if (!content) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: testProviderId,
|
||||
content,
|
||||
};
|
||||
})
|
||||
.filter(Boolean as any as ExcludesNull);
|
||||
}
|
@ -5,6 +5,7 @@ import { FileSearchList } from './FileSearchList';
|
||||
|
||||
const meta = {
|
||||
component: FileSearchList,
|
||||
title: 'Sidebar/FileSearchList',
|
||||
args: {
|
||||
onNewStory: fn(),
|
||||
},
|
||||
|
@ -4,6 +4,7 @@ import { FileSearchListLoadingSkeleton } from './FileSearchListSkeleton';
|
||||
|
||||
const meta = {
|
||||
component: FileSearchListLoadingSkeleton,
|
||||
title: 'Sidebar/FileSearchListLoadingSkeleton',
|
||||
} satisfies Meta<typeof FileSearchListLoadingSkeleton>;
|
||||
|
||||
export default meta;
|
||||
|
@ -8,6 +8,7 @@ import { FileSearchModal } from './FileSearchModal';
|
||||
|
||||
const meta = {
|
||||
component: FileSearchModal,
|
||||
title: 'Sidebar/FileSearchModal',
|
||||
args: {
|
||||
open: true,
|
||||
setError: fn(),
|
||||
|
@ -4,6 +4,7 @@ import { FilterToggle } from './FilterToggle';
|
||||
|
||||
export default {
|
||||
component: FilterToggle,
|
||||
title: 'Sidebar/FilterToggle',
|
||||
args: {
|
||||
active: false,
|
||||
onClick: fn(),
|
||||
|
@ -4,6 +4,7 @@ import { IconSymbols } from './IconSymbols';
|
||||
|
||||
const meta = {
|
||||
component: IconSymbols,
|
||||
title: 'Sidebar/IconSymbols',
|
||||
} satisfies Meta<typeof IconSymbols>;
|
||||
|
||||
export default meta;
|
||||
|
89
code/core/src/manager/components/sidebar/LegacyRender.tsx
Normal file
89
code/core/src/manager/components/sidebar/LegacyRender.tsx
Normal file
@ -0,0 +1,89 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Button } from '@storybook/core/components';
|
||||
import { styled } from '@storybook/core/theming';
|
||||
import { EyeIcon, PlayHollowIcon, StopAltHollowIcon } from '@storybook/icons';
|
||||
|
||||
import type { TestProviders } from '@storybook/core/core-events';
|
||||
import { useStorybookApi } from '@storybook/core/manager-api';
|
||||
|
||||
const Info = styled.div({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
marginLeft: 6,
|
||||
});
|
||||
|
||||
const Actions = styled.div({
|
||||
display: 'flex',
|
||||
gap: 6,
|
||||
});
|
||||
|
||||
const TitleWrapper = styled.div<{ crashed?: boolean }>(({ crashed, theme }) => ({
|
||||
fontSize: theme.typography.size.s1,
|
||||
fontWeight: crashed ? 'bold' : 'normal',
|
||||
color: crashed ? theme.color.negativeText : theme.color.defaultText,
|
||||
}));
|
||||
|
||||
const DescriptionWrapper = styled.div(({ theme }) => ({
|
||||
fontSize: theme.typography.size.s1,
|
||||
color: theme.barTextColor,
|
||||
}));
|
||||
|
||||
export const LegacyRender = ({ ...state }: TestProviders[keyof TestProviders]) => {
|
||||
const Description = state.description!;
|
||||
const Title = state.title!;
|
||||
const api = useStorybookApi();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Info>
|
||||
<TitleWrapper crashed={state.crashed} id="testing-module-title">
|
||||
<Title {...state} />
|
||||
</TitleWrapper>
|
||||
<DescriptionWrapper id="testing-module-description">
|
||||
<Description {...state} />
|
||||
</DescriptionWrapper>
|
||||
</Info>
|
||||
|
||||
<Actions>
|
||||
{state.watchable && (
|
||||
<Button
|
||||
aria-label={`${state.watching ? 'Disable' : 'Enable'} watch mode for ${name}`}
|
||||
variant="ghost"
|
||||
padding="small"
|
||||
active={state.watching}
|
||||
onClick={() => api.setTestProviderWatchMode(state.id, !state.watching)}
|
||||
disabled={state.crashed || state.running}
|
||||
>
|
||||
<EyeIcon />
|
||||
</Button>
|
||||
)}
|
||||
{state.runnable && (
|
||||
<>
|
||||
{state.running && state.cancellable ? (
|
||||
<Button
|
||||
aria-label={`Stop ${name}`}
|
||||
variant="ghost"
|
||||
padding="small"
|
||||
onClick={() => api.cancelTestProvider(state.id)}
|
||||
disabled={state.cancelling}
|
||||
>
|
||||
<StopAltHollowIcon />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
aria-label={`Start ${state.name}`}
|
||||
variant="ghost"
|
||||
padding="small"
|
||||
onClick={() => api.runTestProvider(state.id)}
|
||||
disabled={state.crashed || state.running}
|
||||
>
|
||||
<PlayHollowIcon />
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Actions>
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,7 +1,7 @@
|
||||
import type { ComponentProps, FC } from 'react';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import type { Button, TooltipLinkListLink } from '@storybook/core/components';
|
||||
import type { Button } from '@storybook/core/components';
|
||||
import { IconButton, TooltipLinkList, WithTooltip } from '@storybook/core/components';
|
||||
import { styled } from '@storybook/core/theming';
|
||||
import { CloseIcon, CogIcon } from '@storybook/icons';
|
||||
@ -55,28 +55,11 @@ const MenuButtonGroup = styled.div({
|
||||
gap: 4,
|
||||
});
|
||||
|
||||
type ClickHandler = TooltipLinkListLink['onClick'];
|
||||
|
||||
const SidebarMenuList: FC<{
|
||||
menu: MenuList;
|
||||
onHide: () => void;
|
||||
}> = ({ menu, onHide }) => {
|
||||
const links = useMemo(
|
||||
() =>
|
||||
menu.map((group) =>
|
||||
group.map(({ onClick, ...rest }) => ({
|
||||
...rest,
|
||||
onClick: ((event, item) => {
|
||||
if (onClick) {
|
||||
onClick(event, item);
|
||||
}
|
||||
onHide();
|
||||
}) as ClickHandler,
|
||||
}))
|
||||
),
|
||||
[menu, onHide]
|
||||
);
|
||||
return <TooltipLinkList links={links} />;
|
||||
onClick: () => void;
|
||||
}> = ({ menu, onClick }) => {
|
||||
return <TooltipLinkList links={menu} onClick={onClick} />;
|
||||
};
|
||||
|
||||
export interface SidebarMenuProps {
|
||||
@ -118,7 +101,7 @@ export const SidebarMenu: FC<SidebarMenuProps> = ({ menu, isHighlighted, onClick
|
||||
<WithTooltip
|
||||
placement="top"
|
||||
closeOnOutsideClick
|
||||
tooltip={({ onHide }) => <SidebarMenuList onHide={onHide} menu={menu} />}
|
||||
tooltip={({ onHide }) => <SidebarMenuList onClick={onHide} menu={menu} />}
|
||||
onVisibleChange={setIsTooltipVisible}
|
||||
>
|
||||
<SidebarIconButton
|
||||
|
@ -21,12 +21,13 @@ import { useStorybookApi } from '@storybook/core/manager-api';
|
||||
|
||||
import { transparentize } from 'polished';
|
||||
|
||||
import type { NormalLink } from '../../../components/components/tooltip/TooltipLinkList';
|
||||
import type { getStateType } from '../../utils/tree';
|
||||
import type { RefType } from './types';
|
||||
|
||||
const { document, window: globalWindow } = global;
|
||||
|
||||
export type ClickHandler = TooltipLinkListLink['onClick'];
|
||||
export type ClickHandler = NormalLink['onClick'];
|
||||
export interface IndicatorIconProps {
|
||||
type: ReturnType<typeof getStateType>;
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import { fn } from '@storybook/test';
|
||||
|
||||
import { ManagerContext } from '@storybook/core/manager-api';
|
||||
|
||||
import { standardData as standardHeaderData } from './Heading.stories';
|
||||
@ -8,6 +10,15 @@ import { Ref } from './Refs';
|
||||
import { mockDataset } from './mockdata';
|
||||
import type { RefType } from './types';
|
||||
|
||||
const managerContext = {
|
||||
state: { docsOptions: {}, testProviders: {} },
|
||||
api: {
|
||||
on: fn().mockName('api::on'),
|
||||
off: fn().mockName('api::off'),
|
||||
getElements: fn(() => ({})),
|
||||
},
|
||||
} as any;
|
||||
|
||||
export default {
|
||||
component: Ref,
|
||||
title: 'Sidebar/Refs',
|
||||
@ -16,7 +27,7 @@ export default {
|
||||
globals: { sb_theme: 'side-by-side' },
|
||||
decorators: [
|
||||
(storyFn: any) => (
|
||||
<ManagerContext.Provider value={{ state: { docsOptions: {} } } as any}>
|
||||
<ManagerContext.Provider value={managerContext}>
|
||||
<IconSymbols />
|
||||
{storyFn()}
|
||||
</ManagerContext.Provider>
|
||||
|
@ -33,6 +33,7 @@ const managerContext: any = {
|
||||
autodocs: 'tag',
|
||||
docsMode: false,
|
||||
},
|
||||
testProviders: {},
|
||||
},
|
||||
api: {
|
||||
emit: fn().mockName('api::emit'),
|
||||
|
@ -24,7 +24,6 @@ import { Search } from './Search';
|
||||
import { SearchResults } from './SearchResults';
|
||||
import { SidebarBottom } from './SidebarBottom';
|
||||
import { TagsFilter } from './TagsFilter';
|
||||
import { TEST_PROVIDER_ID } from './Tree';
|
||||
import type { CombinedDataset, Selection } from './types';
|
||||
import { useLastViewed } from './useLastViewed';
|
||||
|
||||
|
@ -1,37 +1,68 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Addon_TypesEnum } from '@storybook/core/types';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import { fn } from '@storybook/test';
|
||||
|
||||
import { type API, ManagerContext } from '@storybook/core/manager-api';
|
||||
|
||||
import { SidebarBottomBase } from './SidebarBottom';
|
||||
|
||||
const managerContext: any = {
|
||||
state: {
|
||||
docsOptions: {
|
||||
defaultName: 'Docs',
|
||||
autodocs: 'tag',
|
||||
docsMode: false,
|
||||
},
|
||||
testProviders: {
|
||||
'component-tests': {
|
||||
type: Addon_TypesEnum.experimental_TEST_PROVIDER,
|
||||
id: 'component-tests',
|
||||
title: () => 'Component tests',
|
||||
description: () => 'Ran 2 seconds ago',
|
||||
runnable: true,
|
||||
watchable: true,
|
||||
},
|
||||
'visual-tests': {
|
||||
type: Addon_TypesEnum.experimental_TEST_PROVIDER,
|
||||
id: 'visual-tests',
|
||||
title: () => 'Visual tests',
|
||||
description: () => 'Not run',
|
||||
runnable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
api: {
|
||||
on: fn().mockName('api::on'),
|
||||
off: fn().mockName('api::off'),
|
||||
updateTestProviderState: fn(),
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
component: SidebarBottomBase,
|
||||
title: 'Sidebar/SidebarBottom',
|
||||
args: {
|
||||
isDevelopment: true,
|
||||
|
||||
api: {
|
||||
on: fn(),
|
||||
off: fn(),
|
||||
clearNotification: fn(),
|
||||
updateTestProviderState: fn(),
|
||||
emit: fn(),
|
||||
experimental_setFilter: fn(),
|
||||
getChannel: fn(),
|
||||
getElements: fn(() => ({
|
||||
'component-tests': {
|
||||
type: Addon_TypesEnum.experimental_TEST_PROVIDER,
|
||||
id: 'component-tests',
|
||||
title: () => 'Component tests',
|
||||
description: () => 'Ran 2 seconds ago',
|
||||
runnable: true,
|
||||
watchable: true,
|
||||
},
|
||||
'visual-tests': {
|
||||
type: Addon_TypesEnum.experimental_TEST_PROVIDER,
|
||||
id: 'visual-tests',
|
||||
title: () => 'Visual tests',
|
||||
description: () => 'Not run',
|
||||
runnable: true,
|
||||
},
|
||||
})),
|
||||
},
|
||||
getElements: fn(() => ({})),
|
||||
} as any as API,
|
||||
},
|
||||
};
|
||||
decorators: [
|
||||
(storyFn) => (
|
||||
<ManagerContext.Provider value={managerContext}>{storyFn()}</ManagerContext.Provider>
|
||||
),
|
||||
],
|
||||
} as Meta<typeof SidebarBottomBase>;
|
||||
|
||||
export const Errors = {
|
||||
args: {
|
||||
|
@ -105,17 +105,7 @@ export const SidebarBottomBase = ({
|
||||
const wrapperRef = useRef<HTMLDivElement | null>(null);
|
||||
const [warningsActive, setWarningsActive] = useState(false);
|
||||
const [errorsActive, setErrorsActive] = useState(false);
|
||||
const [testProviders, setTestProviders] = useState<TestProviders>(() => {
|
||||
let sessionState: TestProviders = {};
|
||||
try {
|
||||
sessionState = JSON.parse(sessionStorage.getItem(STORAGE_KEY) || '{}');
|
||||
} catch (_) {}
|
||||
return Object.fromEntries(
|
||||
Object.entries(api.getElements(Addon_TypesEnum.experimental_TEST_PROVIDER)).map(
|
||||
([id, config]) => [id, { ...config, ...initialTestProviderState, ...sessionState[id] }]
|
||||
)
|
||||
);
|
||||
});
|
||||
const { testProviders } = useStorybookState();
|
||||
|
||||
const warnings = Object.values(status).filter((statusByAddonId) =>
|
||||
Object.values(statusByAddonId).some((value) => value?.status === 'warn')
|
||||
@ -126,55 +116,6 @@ export const SidebarBottomBase = ({
|
||||
const hasWarnings = warnings.length > 0;
|
||||
const hasErrors = errors.length > 0;
|
||||
|
||||
const updateTestProvider = useCallback(
|
||||
(id: TestProviderId, update: Partial<TestProviderState>) =>
|
||||
setTestProviders((state) => {
|
||||
const newValue = { ...state, [id]: { ...state[id], ...update } };
|
||||
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(newValue));
|
||||
return newValue;
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const clearState = useCallback(
|
||||
({ providerId }: { providerId: TestProviderId }) => {
|
||||
updateTestProvider(providerId, {
|
||||
cancelling: false,
|
||||
running: true,
|
||||
failed: false,
|
||||
crashed: false,
|
||||
progress: undefined,
|
||||
});
|
||||
api.experimental_updateStatus(providerId, (state = {}) =>
|
||||
Object.fromEntries(Object.keys(state).map((key) => [key, null]))
|
||||
);
|
||||
},
|
||||
[api, updateTestProvider]
|
||||
);
|
||||
|
||||
const onRunTests = useCallback(
|
||||
(id: TestProviderId) => {
|
||||
api.emit(TESTING_MODULE_RUN_ALL_REQUEST, { providerId: id });
|
||||
},
|
||||
[api]
|
||||
);
|
||||
|
||||
const onCancelTests = useCallback(
|
||||
(id: TestProviderId) => {
|
||||
updateTestProvider(id, { cancelling: true });
|
||||
api.emit(TESTING_MODULE_CANCEL_TEST_RUN_REQUEST, { providerId: id });
|
||||
},
|
||||
[api, updateTestProvider]
|
||||
);
|
||||
|
||||
const onSetWatchMode = useCallback(
|
||||
(providerId: string, watchMode: boolean) => {
|
||||
updateTestProvider(providerId, { watching: watchMode });
|
||||
api.emit(TESTING_MODULE_WATCH_MODE_REQUEST, { providerId, watchMode });
|
||||
},
|
||||
[api, updateTestProvider]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const spacer = spacerRef.current;
|
||||
const wrapper = wrapperRef.current;
|
||||
@ -196,15 +137,27 @@ export const SidebarBottomBase = ({
|
||||
|
||||
useEffect(() => {
|
||||
const onCrashReport = ({ providerId, ...details }: TestingModuleCrashReportPayload) => {
|
||||
updateTestProvider(providerId, { details, running: false, crashed: true, watching: false });
|
||||
api.updateTestProviderState(providerId, {
|
||||
details,
|
||||
running: false,
|
||||
crashed: true,
|
||||
watching: false,
|
||||
});
|
||||
};
|
||||
|
||||
const clearState = ({ providerId }: { providerId: TestProviderId }) => {
|
||||
api.clearTestProviderState(providerId);
|
||||
api.experimental_updateStatus(providerId, (state = {}) =>
|
||||
Object.fromEntries(Object.keys(state).map((key) => [key, null]))
|
||||
);
|
||||
};
|
||||
|
||||
const onProgressReport = ({ providerId, ...result }: TestingModuleProgressReportPayload) => {
|
||||
if (result.status === 'failed') {
|
||||
updateTestProvider(providerId, { ...result, running: false, failed: true });
|
||||
api.updateTestProviderState(providerId, { ...result, running: false, failed: true });
|
||||
} else {
|
||||
const update = { ...result, running: result.status === 'pending' };
|
||||
updateTestProvider(providerId, update);
|
||||
api.updateTestProviderState(providerId, update);
|
||||
|
||||
const { mapStatusUpdate, ...state } = testProviders[providerId];
|
||||
const statusUpdate = mapStatusUpdate?.({ ...state, ...update });
|
||||
@ -214,18 +167,18 @@ export const SidebarBottomBase = ({
|
||||
}
|
||||
};
|
||||
|
||||
api.getChannel()?.on(TESTING_MODULE_CRASH_REPORT, onCrashReport);
|
||||
api.getChannel()?.on(TESTING_MODULE_RUN_ALL_REQUEST, clearState);
|
||||
api.getChannel()?.on(TESTING_MODULE_PROGRESS_REPORT, onProgressReport);
|
||||
api.on(TESTING_MODULE_CRASH_REPORT, onCrashReport);
|
||||
api.on(TESTING_MODULE_RUN_ALL_REQUEST, clearState);
|
||||
api.on(TESTING_MODULE_PROGRESS_REPORT, onProgressReport);
|
||||
|
||||
return () => {
|
||||
api.getChannel()?.off(TESTING_MODULE_CRASH_REPORT, onCrashReport);
|
||||
api.getChannel()?.off(TESTING_MODULE_PROGRESS_REPORT, onProgressReport);
|
||||
api.getChannel()?.off(TESTING_MODULE_RUN_ALL_REQUEST, clearState);
|
||||
api.off(TESTING_MODULE_CRASH_REPORT, onCrashReport);
|
||||
api.off(TESTING_MODULE_PROGRESS_REPORT, onProgressReport);
|
||||
api.off(TESTING_MODULE_RUN_ALL_REQUEST, clearState);
|
||||
};
|
||||
}, [api, testProviders, updateTestProvider, clearState]);
|
||||
}, [api, testProviders]);
|
||||
|
||||
const testProvidersArray = Object.values(testProviders);
|
||||
const testProvidersArray = Object.values(testProviders || {});
|
||||
if (!hasWarnings && !hasErrors && !testProvidersArray.length && !notifications.length) {
|
||||
return null;
|
||||
}
|
||||
@ -244,9 +197,6 @@ export const SidebarBottomBase = ({
|
||||
warningCount: warnings.length,
|
||||
warningsActive,
|
||||
setWarningsActive,
|
||||
onRunTests,
|
||||
onCancelTests,
|
||||
onSetWatchMode,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
@ -2,7 +2,8 @@ import { createContext, useContext } from 'react';
|
||||
|
||||
import type { API_StatusObject, API_StatusState, API_StatusValue, StoryId } from '@storybook/types';
|
||||
|
||||
import type { StoriesHash } from '../../../manager-api';
|
||||
import type { StoriesHash } from '@storybook/core/manager-api';
|
||||
|
||||
import type { Item } from '../../container/Sidebar';
|
||||
import { getDescendantIds } from '../../utils/tree';
|
||||
|
||||
|
@ -5,6 +5,7 @@ import { TagsFilter } from './TagsFilter';
|
||||
|
||||
const meta = {
|
||||
component: TagsFilter,
|
||||
title: 'Sidebar/TagsFilter',
|
||||
tags: ['haha'],
|
||||
args: {
|
||||
api: {
|
||||
|
@ -5,6 +5,7 @@ import { TagsFilterPanel } from './TagsFilterPanel';
|
||||
|
||||
const meta = {
|
||||
component: TagsFilterPanel,
|
||||
title: 'Sidebar/TagsFilterPanel',
|
||||
args: {
|
||||
toggleTag: fn(),
|
||||
api: {
|
||||
|
@ -5,6 +5,7 @@ import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { fn, userEvent } from '@storybook/test';
|
||||
|
||||
import type { TestProviders } from '@storybook/core/core-events';
|
||||
import { ManagerContext } from '@storybook/core/manager-api';
|
||||
|
||||
import { TestingModule } from './TestingModule';
|
||||
|
||||
@ -23,8 +24,13 @@ const testProviders: TestProviders[keyof TestProviders][] = [
|
||||
type: Addon_TypesEnum.experimental_TEST_PROVIDER,
|
||||
id: 'component-tests',
|
||||
name: 'Component tests',
|
||||
title: () => 'Component tests',
|
||||
description: () => 'Ran 2 seconds ago',
|
||||
render: () => (
|
||||
<>
|
||||
Component tests
|
||||
<br />
|
||||
Ran 2 seconds ago
|
||||
</>
|
||||
),
|
||||
runnable: true,
|
||||
watchable: true,
|
||||
...baseState,
|
||||
@ -33,8 +39,13 @@ const testProviders: TestProviders[keyof TestProviders][] = [
|
||||
type: Addon_TypesEnum.experimental_TEST_PROVIDER,
|
||||
id: 'visual-tests',
|
||||
name: 'Visual tests',
|
||||
title: () => 'Visual tests',
|
||||
description: () => 'Not run',
|
||||
render: () => (
|
||||
<>
|
||||
Visual tests
|
||||
<br />
|
||||
Not run
|
||||
</>
|
||||
),
|
||||
runnable: true,
|
||||
...baseState,
|
||||
},
|
||||
@ -42,15 +53,30 @@ const testProviders: TestProviders[keyof TestProviders][] = [
|
||||
type: Addon_TypesEnum.experimental_TEST_PROVIDER,
|
||||
id: 'linting',
|
||||
name: 'Linting',
|
||||
title: () => 'Linting',
|
||||
description: () => 'Watching for changes',
|
||||
render: () => (
|
||||
<>
|
||||
Linting
|
||||
<br />
|
||||
Watching for changes
|
||||
</>
|
||||
),
|
||||
...baseState,
|
||||
watching: true,
|
||||
},
|
||||
];
|
||||
|
||||
const managerContext: any = {
|
||||
api: {
|
||||
runTestProvider: fn().mockName('api::runTestProvider'),
|
||||
cancelTestProvider: fn().mockName('api::cancelTestProvider'),
|
||||
updateTestProviderState: fn().mockName('api::updateTestProviderState'),
|
||||
setTestProviderWatchMode: fn().mockName('api::setTestProviderWatchMode'),
|
||||
},
|
||||
};
|
||||
|
||||
const meta = {
|
||||
component: TestingModule,
|
||||
title: 'Sidebar/TestingModule',
|
||||
args: {
|
||||
testProviders,
|
||||
errorCount: 0,
|
||||
@ -59,11 +85,11 @@ const meta = {
|
||||
warningCount: 0,
|
||||
warningsActive: false,
|
||||
setWarningsActive: fn(),
|
||||
onRunTests: fn(),
|
||||
onCancelTests: fn(),
|
||||
onSetWatchMode: fn(),
|
||||
},
|
||||
decorators: [
|
||||
(storyFn) => (
|
||||
<ManagerContext.Provider value={managerContext}>{storyFn()}</ManagerContext.Provider>
|
||||
),
|
||||
(StoryFn) => (
|
||||
<div style={{ maxWidth: 232 }}>
|
||||
<StoryFn />
|
||||
@ -180,8 +206,13 @@ export const Crashed: Story = {
|
||||
testProviders: [
|
||||
{
|
||||
...testProviders[0],
|
||||
title: () => "Component tests didn't complete",
|
||||
description: () => 'Problems!',
|
||||
render: () => (
|
||||
<>
|
||||
Component tests didn't complete
|
||||
<br />
|
||||
Problems!
|
||||
</>
|
||||
),
|
||||
crashed: true,
|
||||
},
|
||||
...testProviders.slice(1),
|
||||
|
@ -1,18 +1,14 @@
|
||||
import React, { type SyntheticEvent, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { Button, TooltipNote } from '@storybook/core/components';
|
||||
import { WithTooltip } from '@storybook/core/components';
|
||||
import { keyframes, styled } from '@storybook/core/theming';
|
||||
import {
|
||||
ChevronSmallUpIcon,
|
||||
EyeIcon,
|
||||
PlayAllHollowIcon,
|
||||
PlayHollowIcon,
|
||||
StopAltHollowIcon,
|
||||
} from '@storybook/icons';
|
||||
import { ChevronSmallUpIcon, PlayAllHollowIcon } from '@storybook/icons';
|
||||
|
||||
import type { TestProviders } from '@storybook/core/core-events';
|
||||
import { useStorybookApi } from '@storybook/core/manager-api';
|
||||
|
||||
import { WithTooltip } from '../../../components/components/tooltip/WithTooltip';
|
||||
import { LegacyRender } from './LegacyRender';
|
||||
|
||||
const DEFAULT_HEIGHT = 500;
|
||||
|
||||
@ -148,43 +144,6 @@ const TestProvider = styled.div({
|
||||
gap: 6,
|
||||
});
|
||||
|
||||
const Info = styled.div({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
marginLeft: 6,
|
||||
});
|
||||
|
||||
const Actions = styled.div({
|
||||
display: 'flex',
|
||||
gap: 6,
|
||||
});
|
||||
|
||||
const TitleWrapper = styled.div<{ crashed?: boolean }>(({ crashed, theme }) => ({
|
||||
fontSize: theme.typography.size.s1,
|
||||
fontWeight: crashed ? 'bold' : 'normal',
|
||||
color: crashed ? theme.color.negativeText : theme.color.defaultText,
|
||||
}));
|
||||
|
||||
const DescriptionWrapper = styled.div(({ theme }) => ({
|
||||
fontSize: theme.typography.size.s1,
|
||||
color: theme.barTextColor,
|
||||
}));
|
||||
|
||||
const DynamicInfo = ({ state }: { state: TestProviders[keyof TestProviders] }) => {
|
||||
const Description = state.description;
|
||||
const Title = state.title;
|
||||
return (
|
||||
<Info>
|
||||
<TitleWrapper crashed={state.crashed} id="testing-module-title">
|
||||
<Title {...state} />
|
||||
</TitleWrapper>
|
||||
<DescriptionWrapper id="testing-module-description">
|
||||
<Description {...state} />
|
||||
</DescriptionWrapper>
|
||||
</Info>
|
||||
);
|
||||
};
|
||||
|
||||
interface TestingModuleProps {
|
||||
testProviders: TestProviders[keyof TestProviders][];
|
||||
errorCount: number;
|
||||
@ -193,9 +152,6 @@ interface TestingModuleProps {
|
||||
warningCount: number;
|
||||
warningsActive: boolean;
|
||||
setWarningsActive: (active: boolean) => void;
|
||||
onRunTests: (providerId: string) => void;
|
||||
onCancelTests: (providerId: string) => void;
|
||||
onSetWatchMode: (providerId: string, watchMode: boolean) => void;
|
||||
}
|
||||
|
||||
export const TestingModule = ({
|
||||
@ -206,10 +162,8 @@ export const TestingModule = ({
|
||||
warningCount,
|
||||
warningsActive,
|
||||
setWarningsActive,
|
||||
onRunTests,
|
||||
onCancelTests,
|
||||
onSetWatchMode,
|
||||
}: TestingModuleProps) => {
|
||||
const api = useStorybookApi();
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const [maxHeight, setMaxHeight] = useState(DEFAULT_HEIGHT);
|
||||
@ -243,50 +197,14 @@ export const TestingModule = ({
|
||||
}}
|
||||
>
|
||||
<Content ref={contentRef}>
|
||||
{testProviders.map((state) => (
|
||||
<TestProvider key={state.id} data-module-id={state.id}>
|
||||
<DynamicInfo state={state} />
|
||||
<Actions>
|
||||
{state.watchable && (
|
||||
<Button
|
||||
aria-label={`${state.watching ? 'Disable' : 'Enable'} watch mode for ${state.name}`}
|
||||
variant="ghost"
|
||||
padding="small"
|
||||
active={state.watching}
|
||||
onClick={() => onSetWatchMode(state.id, !state.watching)}
|
||||
disabled={state.crashed || state.running}
|
||||
>
|
||||
<EyeIcon />
|
||||
</Button>
|
||||
)}
|
||||
{state.runnable && (
|
||||
<>
|
||||
{state.running && state.cancellable ? (
|
||||
<Button
|
||||
aria-label={`Stop ${state.name}`}
|
||||
variant="ghost"
|
||||
padding="small"
|
||||
onClick={() => onCancelTests(state.id)}
|
||||
disabled={state.cancelling}
|
||||
>
|
||||
<StopAltHollowIcon />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
aria-label={`Start ${state.name}`}
|
||||
variant="ghost"
|
||||
padding="small"
|
||||
onClick={() => onRunTests(state.id)}
|
||||
disabled={state.crashed || state.running}
|
||||
>
|
||||
<PlayHollowIcon />
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Actions>
|
||||
</TestProvider>
|
||||
))}
|
||||
{testProviders.map((state) => {
|
||||
const { render: Render } = state;
|
||||
return (
|
||||
<TestProvider key={state.id} data-module-id={state.id}>
|
||||
{Render ? <Render {...state} /> : <LegacyRender {...state} />}
|
||||
</TestProvider>
|
||||
);
|
||||
})}
|
||||
</Content>
|
||||
</Collapsible>
|
||||
|
||||
@ -299,7 +217,7 @@ export const TestingModule = ({
|
||||
e.stopPropagation();
|
||||
testProviders
|
||||
.filter((state) => !state.crashed && !state.running && state.runnable)
|
||||
.forEach(({ id }) => onRunTests(id));
|
||||
.forEach(({ id }) => api.runTestProvider(id));
|
||||
}}
|
||||
disabled={running}
|
||||
>
|
||||
|
@ -2,9 +2,9 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { expect, within } from '@storybook/test';
|
||||
import { expect, fn, userEvent, within } from '@storybook/test';
|
||||
|
||||
import type { ComponentEntry, IndexHash } from '@storybook/core/manager-api';
|
||||
import { type ComponentEntry, type IndexHash, ManagerContext } from '@storybook/core/manager-api';
|
||||
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
@ -12,6 +12,55 @@ import { DEFAULT_REF_ID } from './Sidebar';
|
||||
import { Tree } from './Tree';
|
||||
import { index } from './mockdata.large';
|
||||
|
||||
const managerContext: any = {
|
||||
state: {
|
||||
docsOptions: {
|
||||
defaultName: 'Docs',
|
||||
autodocs: 'tag',
|
||||
docsMode: false,
|
||||
},
|
||||
testProviders: {
|
||||
'component-tests': {
|
||||
type: 'experimental_TEST_PROVIDER',
|
||||
id: 'component-tests',
|
||||
render: () => 'Component tests',
|
||||
sidebarContextMenu: () => <div>TEST_PROVIDER_CONTEXT_CONTENT</div>,
|
||||
runnable: true,
|
||||
watchable: true,
|
||||
},
|
||||
'visual-tests': {
|
||||
type: 'experimental_TEST_PROVIDER',
|
||||
id: 'visual-tests',
|
||||
render: () => 'Visual tests',
|
||||
sidebarContextMenu: () => null,
|
||||
runnable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
api: {
|
||||
on: fn().mockName('api::on'),
|
||||
off: fn().mockName('api::off'),
|
||||
emit: fn().mockName('api::emit'),
|
||||
getElements: fn(() => ({
|
||||
'component-tests': {
|
||||
type: 'experimental_TEST_PROVIDER',
|
||||
id: 'component-tests',
|
||||
render: () => 'Component tests',
|
||||
sidebarContextMenu: () => <div>TEST_PROVIDER_CONTEXT_CONTENT</div>,
|
||||
runnable: true,
|
||||
watchable: true,
|
||||
},
|
||||
'visual-tests': {
|
||||
type: 'experimental_TEST_PROVIDER',
|
||||
id: 'visual-tests',
|
||||
render: () => 'Visual tests',
|
||||
sidebarContextMenu: () => null,
|
||||
runnable: true,
|
||||
},
|
||||
})),
|
||||
},
|
||||
};
|
||||
|
||||
const meta = {
|
||||
component: Tree,
|
||||
title: 'Sidebar/Tree',
|
||||
@ -35,6 +84,11 @@ const meta = {
|
||||
},
|
||||
chromatic: { viewports: [380] },
|
||||
},
|
||||
decorators: [
|
||||
(storyFn) => (
|
||||
<ManagerContext.Provider value={managerContext}>{storyFn()}</ManagerContext.Provider>
|
||||
),
|
||||
],
|
||||
} as Meta<typeof Tree>;
|
||||
|
||||
export default meta;
|
||||
@ -233,3 +287,41 @@ export const SkipToCanvasLinkFocused: Story = {
|
||||
await expect(link).toBeVisible();
|
||||
},
|
||||
};
|
||||
|
||||
// SkipToCanvas Link only shows on desktop widths
|
||||
export const WithContextContent: Story = {
|
||||
...DocsOnlySingleStoryComponents,
|
||||
parameters: {
|
||||
chromatic: { viewports: [1280] },
|
||||
viewport: {
|
||||
options: {
|
||||
desktop: {
|
||||
name: 'Desktop',
|
||||
styles: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
globals: {
|
||||
viewport: { value: 'desktop' },
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const screen = await within(canvasElement);
|
||||
|
||||
const link = await screen.findByText('TooltipBuildList');
|
||||
await userEvent.hover(link);
|
||||
|
||||
const contextButton = await screen.findByTestId('context-menu');
|
||||
await userEvent.click(contextButton);
|
||||
|
||||
const body = await within(document.body);
|
||||
|
||||
const tooltip = await body.findByTestId('tooltip');
|
||||
|
||||
await expect(tooltip).toBeVisible();
|
||||
expect(tooltip).toHaveTextContent('TEST_PROVIDER_CONTEXT_CONTENT');
|
||||
},
|
||||
};
|
||||
|
@ -1,8 +1,9 @@
|
||||
import type { ComponentProps, MutableRefObject } from 'react';
|
||||
import type { ComponentProps, FC, MutableRefObject } from 'react';
|
||||
import React, { useCallback, useMemo, useRef } from 'react';
|
||||
|
||||
import { Button, IconButton, TooltipLinkList, WithTooltip } from '@storybook/core/components';
|
||||
import { Button, IconButton, ListItem } from '@storybook/core/components';
|
||||
import { styled, useTheme } from '@storybook/core/theming';
|
||||
import { type API_HashEntry, type API_StatusValue, type StoryId } from '@storybook/core/types';
|
||||
import {
|
||||
CollapseIcon as CollapseIconSvg,
|
||||
ExpandAltIcon,
|
||||
@ -11,7 +12,6 @@ import {
|
||||
StatusWarnIcon,
|
||||
SyncIcon,
|
||||
} from '@storybook/icons';
|
||||
import type { API_HashEntry, API_StatusValue, StoryId } from '@storybook/types';
|
||||
|
||||
import { PRELOAD_ENTRIES } from '@storybook/core/core-events';
|
||||
import { useStorybookApi } from '@storybook/core/manager-api';
|
||||
@ -26,6 +26,7 @@ import type {
|
||||
|
||||
import { transparentize } from 'polished';
|
||||
|
||||
import type { Link } from '../../../components/components/tooltip/TooltipLinkList';
|
||||
import { getGroupStatus, getHighestStatus, statusMapping } from '../../utils/status';
|
||||
import {
|
||||
createId,
|
||||
@ -35,6 +36,7 @@ import {
|
||||
isStoryHoistable,
|
||||
} from '../../utils/tree';
|
||||
import { useLayout } from '../layout/LayoutProvider';
|
||||
import { useContextMenu } from './ContextMenu';
|
||||
import { IconSymbols, UseSymbol } from './IconSymbols';
|
||||
import { StatusButton } from './StatusButton';
|
||||
import { StatusContext, useStatusSummary } from './StatusContext';
|
||||
@ -44,8 +46,7 @@ import type { Highlight, Item } from './types';
|
||||
import type { ExpandAction, ExpandedState } from './useExpanded';
|
||||
import { useExpanded } from './useExpanded';
|
||||
|
||||
export const TEST_ADDON_ID = 'storybook/test';
|
||||
export const TEST_PROVIDER_ID = `${TEST_ADDON_ID}/test-provider`;
|
||||
export type ExcludesNull = <T>(x: T | null) => x is T;
|
||||
|
||||
const Container = styled.div<{ hasOrphans: boolean }>((props) => ({
|
||||
marginTop: props.hasOrphans ? 20 : 0,
|
||||
@ -84,6 +85,22 @@ export const LeafNodeStyleWrapper = styled.div(({ theme }) => ({
|
||||
outline: 'none',
|
||||
},
|
||||
|
||||
'& [data-displayed="off"]': {
|
||||
visibility: 'hidden',
|
||||
},
|
||||
|
||||
'&:hover [data-displayed="off"]': {
|
||||
visibility: 'visible',
|
||||
},
|
||||
|
||||
'& [data-displayed="on"] + *': {
|
||||
display: 'none',
|
||||
},
|
||||
|
||||
'&:hover [data-displayed="off"] + *': {
|
||||
display: 'none',
|
||||
},
|
||||
|
||||
'&[data-selected="true"]': {
|
||||
color: theme.color.lightest,
|
||||
background: theme.color.secondary,
|
||||
@ -139,6 +156,40 @@ interface NodeProps {
|
||||
collapsedData: Record<string, API_HashEntry>;
|
||||
}
|
||||
|
||||
const SuccessStatusIcon: FC<ComponentProps<typeof StatusPassIcon>> = (props) => {
|
||||
const theme = useTheme();
|
||||
return <StatusPassIcon {...props} color={theme.color.positive} />;
|
||||
};
|
||||
|
||||
const ErrorStatusIcon: FC<ComponentProps<typeof StatusFailIcon>> = (props) => {
|
||||
const theme = useTheme();
|
||||
return <StatusFailIcon {...props} color={theme.color.negative} />;
|
||||
};
|
||||
|
||||
const WarnStatusIcon: FC<ComponentProps<typeof StatusWarnIcon>> = (props) => {
|
||||
const theme = useTheme();
|
||||
return <StatusWarnIcon {...props} color={theme.color.warning} />;
|
||||
};
|
||||
|
||||
const PendingStatusIcon: FC<ComponentProps<typeof SyncIcon>> = (props) => {
|
||||
const theme = useTheme();
|
||||
return <SyncIcon {...props} size={12} color={theme.color.defaultText} />;
|
||||
};
|
||||
|
||||
const StatusIconMap = {
|
||||
success: <SuccessStatusIcon />,
|
||||
error: <ErrorStatusIcon />,
|
||||
warn: <WarnStatusIcon />,
|
||||
pending: <PendingStatusIcon />,
|
||||
unknown: null,
|
||||
};
|
||||
|
||||
export const ContextMenu = {
|
||||
ListItem,
|
||||
};
|
||||
|
||||
const statusOrder: API_StatusValue[] = ['success', 'error', 'warn', 'pending', 'unknown'];
|
||||
|
||||
const Node = React.memo<NodeProps>(function Node({
|
||||
item,
|
||||
status,
|
||||
@ -153,26 +204,82 @@ const Node = React.memo<NodeProps>(function Node({
|
||||
isExpanded,
|
||||
setExpanded,
|
||||
onSelectStoryId,
|
||||
collapsedData,
|
||||
api,
|
||||
}) {
|
||||
const { isDesktop, isMobile, setMobileMenuOpen } = useLayout();
|
||||
const theme = useTheme();
|
||||
const { counts, statuses } = useStatusSummary(item);
|
||||
|
||||
if (!isDisplayed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const statusLinks = useMemo<Link[]>(() => {
|
||||
if (item.type === 'story' || item.type === 'docs') {
|
||||
return 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,
|
||||
'aria-label': `Test status for ${value.title}: ${value.status}`,
|
||||
icon: StatusIconMap[value.status],
|
||||
onClick: () => {
|
||||
onSelectStoryId(item.id);
|
||||
value.onClick?.();
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
if (item.type === 'component' || item.type === 'group') {
|
||||
const links: Link[] = [];
|
||||
if (counts.error) {
|
||||
links.push({
|
||||
id: 'errors',
|
||||
icon: StatusIconMap.error,
|
||||
title: `${counts.error} ${counts.error === 1 ? 'story' : 'stories'} with errors`,
|
||||
onClick: () => {
|
||||
const [firstStoryId, [firstError]] = Object.entries(statuses.error)[0];
|
||||
onSelectStoryId(firstStoryId);
|
||||
firstError.onClick?.();
|
||||
},
|
||||
});
|
||||
}
|
||||
if (counts.warn) {
|
||||
links.push({
|
||||
id: 'warnings',
|
||||
icon: StatusIconMap.warn,
|
||||
title: `${counts.warn} ${counts.warn === 1 ? 'story' : 'stories'} with warnings`,
|
||||
onClick: () => {
|
||||
const [firstStoryId, [firstWarning]] = Object.entries(statuses.warn)[0];
|
||||
onSelectStoryId(firstStoryId);
|
||||
firstWarning.onClick?.();
|
||||
},
|
||||
});
|
||||
}
|
||||
return links;
|
||||
}
|
||||
|
||||
return [];
|
||||
}, [
|
||||
counts.error,
|
||||
counts.warn,
|
||||
item.id,
|
||||
item.type,
|
||||
onSelectStoryId,
|
||||
status,
|
||||
statuses.error,
|
||||
statuses.warn,
|
||||
]);
|
||||
|
||||
const id = createId(item.id, refId);
|
||||
const contextMenu = useContextMenu(item, statusLinks, api);
|
||||
|
||||
if (item.type === 'story' || item.type === 'docs') {
|
||||
const LeafNode = item.type === 'docs' ? DocumentNode : StoryNode;
|
||||
|
||||
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 (
|
||||
<LeafNodeStyleWrapper
|
||||
key={id}
|
||||
@ -183,6 +290,7 @@ const Node = React.memo<NodeProps>(function Node({
|
||||
data-parent-id={item.parent}
|
||||
data-nodetype={item.type === 'docs' ? 'document' : 'story'}
|
||||
data-highlightable={isDisplayed}
|
||||
onMouseEnter={contextMenu.onMouseEnter}
|
||||
>
|
||||
<LeafNode
|
||||
// @ts-expect-error (non strict)
|
||||
@ -208,49 +316,17 @@ const Node = React.memo<NodeProps>(function Node({
|
||||
<a href="#storybook-preview-wrapper">Skip to canvas</a>
|
||||
</SkipToContentLink>
|
||||
)}
|
||||
{contextMenu.node}
|
||||
{icon ? (
|
||||
<WithTooltip
|
||||
closeOnOutsideClick
|
||||
closeOnTriggerHidden
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
placement="bottom"
|
||||
tooltip={({ onHide }) => (
|
||||
<TooltipLinkList
|
||||
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,
|
||||
'aria-label': `Test status for ${value.title}: ${value.status}`,
|
||||
icon: {
|
||||
success: <StatusPassIcon color={theme.color.positive} />,
|
||||
error: <StatusFailIcon color={theme.color.negative} />,
|
||||
warn: <StatusWarnIcon color={theme.color.warning} />,
|
||||
pending: <SyncIcon size={12} color={theme.color.defaultText} />,
|
||||
unknown: null,
|
||||
}[value.status],
|
||||
onClick: () => {
|
||||
onSelectStoryId(item.id);
|
||||
value.onClick?.();
|
||||
onHide();
|
||||
},
|
||||
}))}
|
||||
/>
|
||||
)}
|
||||
<StatusButton
|
||||
aria-label={`Test status: ${statusValue}`}
|
||||
role="status"
|
||||
type="button"
|
||||
status={statusValue}
|
||||
selectedItem={isSelected}
|
||||
>
|
||||
<StatusButton
|
||||
aria-label={`Test status: ${statusValue}`}
|
||||
role="status"
|
||||
type="button"
|
||||
status={statusValue}
|
||||
selectedItem={isSelected}
|
||||
>
|
||||
{icon}
|
||||
</StatusButton>
|
||||
</WithTooltip>
|
||||
{icon}
|
||||
</StatusButton>
|
||||
) : null}
|
||||
</LeafNodeStyleWrapper>
|
||||
);
|
||||
@ -302,39 +378,6 @@ const Node = React.memo<NodeProps>(function Node({
|
||||
const color = itemStatus ? statusMapping[itemStatus][1] : null;
|
||||
const BranchNode = item.type === 'component' ? ComponentNode : GroupNode;
|
||||
|
||||
const createLinks: (onHide: () => void) => ComponentProps<typeof TooltipLinkList>['links'] = (
|
||||
onHide
|
||||
) => {
|
||||
const links = [];
|
||||
if (counts.error) {
|
||||
links.push({
|
||||
id: 'errors',
|
||||
icon: <StatusFailIcon color={theme.color.negative} />,
|
||||
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: <StatusWarnIcon color={theme.color.gold} />,
|
||||
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 (
|
||||
<LeafNodeStyleWrapper
|
||||
key={id}
|
||||
@ -342,8 +385,9 @@ const Node = React.memo<NodeProps>(function Node({
|
||||
data-ref-id={refId}
|
||||
data-item-id={item.id}
|
||||
data-parent-id={item.parent}
|
||||
data-nodetype={item.type === 'component' ? 'component' : 'group'}
|
||||
data-nodetype={item.type}
|
||||
data-highlightable={isDisplayed}
|
||||
onMouseEnter={contextMenu.onMouseEnter}
|
||||
>
|
||||
<BranchNode
|
||||
id={id}
|
||||
@ -374,19 +418,13 @@ const Node = React.memo<NodeProps>(function Node({
|
||||
{(item.renderLabel as (i: typeof item, api: API) => React.ReactNode)?.(item, api) ||
|
||||
item.name}
|
||||
</BranchNode>
|
||||
{contextMenu.node}
|
||||
{['error', 'warn'].includes(itemStatus) && (
|
||||
<WithTooltip
|
||||
closeOnOutsideClick
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
placement="bottom"
|
||||
tooltip={({ onHide }) => <TooltipLinkList links={createLinks(onHide)} />}
|
||||
>
|
||||
<StatusButton type="button" status={itemStatus}>
|
||||
<svg key="icon" viewBox="0 0 6 6" width="6" height="6" type="dot">
|
||||
<UseSymbol type="dot" />
|
||||
</svg>
|
||||
</StatusButton>
|
||||
</WithTooltip>
|
||||
<StatusButton type="button" status={itemStatus}>
|
||||
<svg key="icon" viewBox="0 0 6 6" width="6" height="6" type="dot">
|
||||
<UseSymbol type="dot" />
|
||||
</svg>
|
||||
</StatusButton>
|
||||
)}
|
||||
</LeafNodeStyleWrapper>
|
||||
);
|
||||
@ -593,6 +631,10 @@ export const Tree = React.memo<{
|
||||
|
||||
const isDisplayed = !item.parent || ancestry[itemId].every((a: string) => expanded[a]);
|
||||
|
||||
if (isDisplayed === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Node
|
||||
api={api}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import type { State } from '../../manager-api/root';
|
||||
import type { State } from '@storybook/core/manager-api';
|
||||
|
||||
export const defaultShortcuts: State['shortcuts'] = {
|
||||
fullScreen: ['F'],
|
||||
|
@ -1,11 +1,12 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
import type { FC, PropsWithChildren, ReactElement, ReactNode } from 'react';
|
||||
|
||||
import type { TestingModuleProgressReportProgress } from '../../core-events';
|
||||
import type { ListItem } from '../../components';
|
||||
import type { TestProviderConfig, TestingModuleProgressReportProgress } from '../../core-events';
|
||||
import type { RenderData as RouterData } from '../../router/types';
|
||||
import type { ThemeVars } from '../../theming/types';
|
||||
import type { API_SidebarOptions } from './api';
|
||||
import type { API_StatusState, API_StatusUpdate } from './api-stories';
|
||||
import type { API_HashEntry, API_StatusState, API_StatusUpdate } from './api-stories';
|
||||
import type {
|
||||
Args,
|
||||
ArgsStoryFn as ArgsStoryFnForFramework,
|
||||
@ -28,6 +29,7 @@ export type Addon_Types = Exclude<
|
||||
Addon_TypesEnum,
|
||||
| Addon_TypesEnum.experimental_PAGE
|
||||
| Addon_TypesEnum.experimental_SIDEBAR_BOTTOM
|
||||
| Addon_TypesEnum.experimental_TEST_PROVIDER
|
||||
| Addon_TypesEnum.experimental_SIDEBAR_TOP
|
||||
>;
|
||||
|
||||
@ -329,7 +331,7 @@ export type Addon_Type =
|
||||
| Addon_WrapperType
|
||||
| Addon_SidebarBottomType
|
||||
| Addon_SidebarTopType
|
||||
| Addon_TestProviderType;
|
||||
| Addon_TestProviderType<Addon_TestProviderState>;
|
||||
export interface Addon_BaseType {
|
||||
/**
|
||||
* The title of the addon. This can be a simple string, but it can also be a
|
||||
@ -472,8 +474,18 @@ export interface Addon_TestProviderType<
|
||||
/** The unique id of the test provider. */
|
||||
id: string;
|
||||
name: string;
|
||||
title: (state: Addon_TestProviderState<Details>) => ReactNode;
|
||||
description: (state: Addon_TestProviderState<Details>) => ReactNode;
|
||||
/** @deprecated Use render instead */
|
||||
title?: (state: TestProviderConfig & Addon_TestProviderState<Details>) => ReactNode;
|
||||
/** @deprecated Use render instead */
|
||||
description?: (state: TestProviderConfig & Addon_TestProviderState<Details>) => ReactNode;
|
||||
render?: (state: TestProviderConfig & Addon_TestProviderState<Details>) => ReactNode;
|
||||
sidebarContextMenu?: (
|
||||
options: {
|
||||
context: API_HashEntry;
|
||||
state: Addon_TestProviderState<Details>;
|
||||
},
|
||||
components: { ListItem: typeof ListItem }
|
||||
) => ReactNode;
|
||||
mapStatusUpdate?: (
|
||||
state: Addon_TestProviderState<Details>
|
||||
) => API_StatusUpdate | ((state: API_StatusState) => API_StatusUpdate);
|
||||
@ -511,7 +523,7 @@ export interface Addon_TypesMapping extends Record<Addon_TypeBaseNames, Addon_Ba
|
||||
[Addon_TypesEnum.experimental_PAGE]: Addon_PageType;
|
||||
[Addon_TypesEnum.experimental_SIDEBAR_BOTTOM]: Addon_SidebarBottomType;
|
||||
[Addon_TypesEnum.experimental_SIDEBAR_TOP]: Addon_SidebarTopType;
|
||||
[Addon_TypesEnum.experimental_TEST_PROVIDER]: Addon_TestProviderType;
|
||||
[Addon_TypesEnum.experimental_TEST_PROVIDER]: Addon_TestProviderType<Addon_TestProviderState>;
|
||||
}
|
||||
|
||||
export type Addon_Loader<API> = (api: API) => void;
|
||||
|
@ -8,6 +8,7 @@ export type SupportedFrameworks =
|
||||
| 'nextjs'
|
||||
| 'preact-vite'
|
||||
| 'preact-webpack5'
|
||||
| 'react-native-web-vite'
|
||||
| 'react-vite'
|
||||
| 'react-webpack5'
|
||||
| 'server-webpack5'
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/builder-manager",
|
||||
"version": "8.5.0-alpha.4",
|
||||
"version": "8.5.0-alpha.9",
|
||||
"description": "Storybook manager builder",
|
||||
"keywords": [
|
||||
"storybook"
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/channels",
|
||||
"version": "8.5.0-alpha.4",
|
||||
"version": "8.5.0-alpha.9",
|
||||
"description": "",
|
||||
"keywords": [
|
||||
"storybook"
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/client-logger",
|
||||
"version": "8.5.0-alpha.4",
|
||||
"version": "8.5.0-alpha.9",
|
||||
"description": "",
|
||||
"keywords": [
|
||||
"storybook"
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/components",
|
||||
"version": "8.5.0-alpha.4",
|
||||
"version": "8.5.0-alpha.9",
|
||||
"description": "Core Storybook Components",
|
||||
"keywords": [
|
||||
"storybook"
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/core-common",
|
||||
"version": "8.5.0-alpha.4",
|
||||
"version": "8.5.0-alpha.9",
|
||||
"description": "Storybook framework-agnostic API",
|
||||
"keywords": [
|
||||
"storybook"
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/core-events",
|
||||
"version": "8.5.0-alpha.4",
|
||||
"version": "8.5.0-alpha.9",
|
||||
"description": "Event names used in storybook core",
|
||||
"keywords": [
|
||||
"storybook"
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/core-server",
|
||||
"version": "8.5.0-alpha.4",
|
||||
"version": "8.5.0-alpha.9",
|
||||
"description": "Storybook framework-agnostic API",
|
||||
"keywords": [
|
||||
"storybook"
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/csf-tools",
|
||||
"version": "8.5.0-alpha.4",
|
||||
"version": "8.5.0-alpha.9",
|
||||
"description": "Parse and manipulate CSF and Storybook config files",
|
||||
"keywords": [
|
||||
"storybook"
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/docs-tools",
|
||||
"version": "8.5.0-alpha.4",
|
||||
"version": "8.5.0-alpha.9",
|
||||
"description": "Shared utility functions for frameworks to implement docs",
|
||||
"keywords": [
|
||||
"storybook"
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/manager-api",
|
||||
"version": "8.5.0-alpha.4",
|
||||
"version": "8.5.0-alpha.9",
|
||||
"description": "Core Storybook Manager API & Context",
|
||||
"keywords": [
|
||||
"storybook"
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/manager",
|
||||
"version": "8.5.0-alpha.4",
|
||||
"version": "8.5.0-alpha.9",
|
||||
"description": "Core Storybook UI",
|
||||
"keywords": [
|
||||
"storybook"
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/node-logger",
|
||||
"version": "8.5.0-alpha.4",
|
||||
"version": "8.5.0-alpha.9",
|
||||
"description": "",
|
||||
"keywords": [
|
||||
"storybook"
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/preview-api",
|
||||
"version": "8.5.0-alpha.4",
|
||||
"version": "8.5.0-alpha.9",
|
||||
"description": "",
|
||||
"keywords": [
|
||||
"storybook"
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/preview",
|
||||
"version": "8.5.0-alpha.4",
|
||||
"version": "8.5.0-alpha.9",
|
||||
"description": "",
|
||||
"keywords": [
|
||||
"storybook"
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/router",
|
||||
"version": "8.5.0-alpha.4",
|
||||
"version": "8.5.0-alpha.9",
|
||||
"description": "Core Storybook Router",
|
||||
"keywords": [
|
||||
"storybook"
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/telemetry",
|
||||
"version": "8.5.0-alpha.4",
|
||||
"version": "8.5.0-alpha.9",
|
||||
"description": "Telemetry logging for crash reports and usage statistics",
|
||||
"keywords": [
|
||||
"storybook"
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/theming",
|
||||
"version": "8.5.0-alpha.4",
|
||||
"version": "8.5.0-alpha.9",
|
||||
"description": "Core Storybook Components",
|
||||
"keywords": [
|
||||
"storybook"
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/types",
|
||||
"version": "8.5.0-alpha.4",
|
||||
"version": "8.5.0-alpha.9",
|
||||
"description": "Core Storybook TS Types",
|
||||
"keywords": [
|
||||
"storybook"
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user