Feat: add storybook setup (#10721)

* feat(storybook): add Storybook setup and Button component stories

- Add Storybook configuration files (.storybook/main.ts, preview.ts, css.d.ts)
- Add Button component stories with interaction testing
- Add Storybook dependencies and scripts to package.json
- Support dark mode in stories via decorator
- Include play functions for automated interaction testing

* ci(storybook): add GitHub Pages deployment workflow

- Add automated deployment workflow for Storybook
- Deploy on push to main when Storybook files change
- Support manual trigger via workflow_dispatch
- Use official GitHub Actions for Pages deployment

* fix(storybook): align Storybook versions and fix TypeScript issues

- Update all @storybook/* packages from ^8.4.7 to ^8.6.14 to match main storybook version
- Fix CSS type declarations in css.d.ts (Record<string, string> instead of Record<string, never>)
- Remove @ts-ignore comments from preview.ts (CSS imports now properly typed)
- Fix darkMode type issue in button.stories.tsx with proper ArgTypes type assertion

* feat(storybook): replace Button stories with Dropdown component stories

- Remove Button stories (shadcn already has documentation)
- Add comprehensive Dropdown component stories showcasing:
  - Default, with value, combobox mode
  - With metadata, disabled, loading states
  - Many options, searchable filtering
  - Dark mode support
- Add store initialization decorator for Storybook
- Include interaction testing with play functions

* feat(storybook): add SettingsPage stories and remove dropdown stories

- Remove dropdown component stories
- Add SettingsPage stories with router, store, and dark mode decorators
- Include variants: default, with general settings, and dark mode

* feat(storybook): fix SettingsPage stories to show full page and add play functions

- Fix router setup to properly render SettingsPage with nested routes
- Add Routes configuration for all settings sub-pages (General, MCP Servers, Global Variables, Shortcuts, Messages)
- Add play functions to test page visibility and navigation
- Add stories for different routes: Default, WithGeneralSettings, NavigateToShortcuts, NavigateToGlobalVariables, DarkMode

* revert(storybook): restore SettingsPage stories to original working version

- Revert to simpler router setup without Routes configuration
- Remove play functions and complex routing
- Restore original three stories: Default, WithGeneralSettings, DarkMode

* feat(storybook): add stories for ShortcutsPage and GlobalVariablesPage with tables

- Add ShortcutsPage stories showing the shortcuts table
- Add GlobalVariablesPage stories showing the global variables table
- Include store setup for shortcuts data
- Add play functions to verify table visibility
- Support dark mode for both pages

* fix(storybook): add QueryClientProvider to GlobalVariablesPage stories

- Add QueryClientProvider decorator to support React Query hooks
- Configure QueryClient with retry disabled for Storybook

* fix(storybook): remove WithGeneralSettings story to fix nested router error

- Remove WithGeneralSettings story that was causing nested Router error
- Keep Default and DarkMode stories only

* feat(storybook): enhance SettingsPage stories with multiple states and logic variations

- Add stories showcasing different store configurations (autoLogin, hasStore)
- Demonstrate conditional General settings visibility logic
- Add interactive sidebar navigation story
- Show full configuration with all features
- Include play functions to verify state-based behavior
- Showcase how page adapts to different user/auth states

* fix(storybook): initialize Zustand stores synchronously in SettingsPage stories

- Set store state before component render instead of in useEffect
- Ensures stores are accessible when SettingsPage component mounts
- Fixes state access errors in Storybook

* feat(storybook): add story to verify store state accessibility

- Add VerifyStoreState story that demonstrates accessing Zustand store state
- Verify store values match expected configuration
- Show that state is accessible from play functions

* fix(storybook): remove router from SettingsPage stories to fix errors

- Remove MemoryRouter decorator that was causing errors
- Keep store setup and dark mode decorators
- Stories now work without router dependency

* fix(storybook): add router back to SettingsPage stories for useNavigate support

- Add MemoryRouter back to support useCustomNavigate hook in PageLayout
- Router is needed for navigation hooks to work properly
- Keep router at decorator level to avoid nested router errors

* fix(storybook): fix router decorator order in SettingsPage stories

- Move router decorator to be outermost (last in array)
- Decorators run bottom-to-top, so router should wrap everything
- Ensures useNavigate context is available to all components

* feat(storybook): add PlaygroundPage story as example for complex page stories

- Add PlaygroundPage story demonstrating how to create stories for complex pages
- Include darkMode toggle control as example for interactive story controls
- Set up decorators for query client, router, and theme switching
- Hide publish elements (Theme buttons, Built with Langflow) in story view
- Center chat title header in story view
- Configure Storybook preview and CSS types

This story serves as a reference for creating stories for full page components
rather than simple UI components, which are already documented in shadcn docs.

* chore(storybook): remove SettingsPage stories

Keep only PlaygroundPage story as the example for complex page stories.

* chore: restore pyproject.toml to match main branch

* chore: restore pyproject.toml to match main branch

* Revert "chore: restore pyproject.toml to match main branch"

This reverts commit a2b75a4028f1f046bc84a8d7999d53a2cb720fc9.

* chore: remove src/frontend/pyproject.toml as it doesn't exist in main

* fix gitignore and add make commands

* update package-json

* chore(storybook): migrate from v8.6.14 to v10.1.0

- Update all Storybook packages to v10.1.0
- Replace @storybook/addon-essentials with @storybook/addon-docs
- Remove @storybook/addon-interactions (moved to core)
- Remove @storybook/blocks and @storybook/test (consolidated)
- Fix import in PlaygroundPage.stories.tsx to use @storybook/react
- Update tsconfig.json moduleResolution to 'bundler' for better compatibility
- Add explicit types configuration for @storybook/react

* fix: update package-lock.json to sync with package.json

* fix: regenerate package-lock.json with all optional dependencies

---------

Co-authored-by: Olfa Maslah <olfamaslah@Olfas-MacBook-Pro.local>
Co-authored-by: Cristhian Zanforlin <criszl@MacBook-Pro-di-Cristhian.local>
Co-authored-by: Olfa Maslah <olfamaslah@macbookpro.war.can.ibm.com>
This commit is contained in:
Wallgau
2025-11-26 15:39:05 -05:00
committed by GitHub
parent 5226daadaa
commit 0ddfed3c43
12 changed files with 4414 additions and 3044 deletions

View File

@@ -2,7 +2,8 @@
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:prettier/recommended"
"plugin:prettier/recommended",
"plugin:storybook/recommended"
],
"plugins": [
"react",
@@ -87,4 +88,4 @@
"node/prefer-promises/dns": "error",
"node/prefer-promises/fs": "error"
}
}
}

49
.github/workflows/deploy-storybook.yml vendored Normal file
View File

@@ -0,0 +1,49 @@
name: Deploy Storybook to GitHub Pages
on:
push:
branches:
- main
paths:
- 'src/frontend/**/*.stories.*'
- 'src/frontend/.storybook/**'
- 'src/frontend/package.json'
- '.github/workflows/deploy-storybook.yml'
workflow_dispatch: # Allow manual trigger
jobs:
deploy:
name: Deploy Storybook
runs-on: ubuntu-latest
permissions:
contents: read
pages: write
id-token: write
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
cache-dependency-path: src/frontend/package-lock.json
- name: Install dependencies
run: cd src/frontend && npm ci
- name: Build Storybook
run: cd src/frontend && npm run build-storybook
- name: Setup Pages
uses: actions/configure-pages@v4
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: src/frontend/storybook-static
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

View File

@@ -5,7 +5,7 @@
FRONTEND_DIR = src/frontend
NPM = npm
.PHONY: install_frontend install_frontendci install_frontendc frontend_deps_check build_frontend run_frontend frontend frontendc format_frontend tests_frontend test_frontend test_frontend_watch test_frontend_coverage test_frontend_verbose test_frontend_ci test_frontend_clean test_frontend_file test_frontend_pattern test_frontend_snapshots test_frontend_config test_frontend_bail test_frontend_silent test_frontend_coverage_open help_frontend
.PHONY: install_frontend install_frontendci install_frontendc frontend_deps_check build_frontend run_frontend frontend frontendc format_frontend tests_frontend test_frontend test_frontend_watch test_frontend_coverage test_frontend_verbose test_frontend_ci test_frontend_clean test_frontend_file test_frontend_pattern test_frontend_snapshots test_frontend_config test_frontend_bail test_frontend_silent test_frontend_coverage_open help_frontend storybook storybook_build storybook_network
######################
# FRONTEND DEPENDENCIES
@@ -166,6 +166,23 @@ test_frontend_coverage_open: test_frontend_coverage ## run tests with coverage a
echo "Coverage report generated at: $(FRONTEND_DIR)/coverage/lcov-report/index.html"; \
fi
######################
# STORYBOOK
######################
storybook: frontend_deps_check ## run Storybook development server and open in browser
@echo "Starting Storybook development server on http://localhost:6006..."
@cd $(FRONTEND_DIR) && $(NPM) run storybook
storybook_build: frontend_deps_check ## build static Storybook
@echo "Building static Storybook..."
@cd $(FRONTEND_DIR) && $(NPM) run build-storybook
@echo "Storybook built to $(FRONTEND_DIR)/storybook-static"
storybook_network: frontend_deps_check ## run Storybook accessible on network (0.0.0.0:6006)
@echo "Starting Storybook development server accessible on network..."
@cd $(FRONTEND_DIR) && $(NPM) run storybook:network
######################
# FRONTEND HELP
######################
@@ -212,5 +229,10 @@ help_frontend: ## show frontend help
@echo " $(GREEN)make test_frontend_snapshots$(NC) - Update Jest snapshots"
@echo " $(GREEN)make test_frontend_config$(NC) - Show Jest configuration"
@echo ''
@echo "$(GREEN)Storybook:$(NC)"
@echo " $(GREEN)make storybook$(NC) - Run Storybook dev server and open in browser"
@echo " $(GREEN)make storybook_build$(NC) - Build static Storybook"
@echo " $(GREEN)make storybook_network$(NC) - Run Storybook accessible on network"
@echo ''
@echo "$(GREEN)═══════════════════════════════════════════════════════════════════$(NC)"
@echo ''

View File

@@ -135,7 +135,7 @@ dependencies = [
"fastparquet>=2024.11.0,<2025.0.0",
"traceloop-sdk>=0.43.1,<1.0.0",
"vlmrun[all]>=0.2.0",
"cuga==0.1.11",
"cuga==0.1.10",
"agent-lifecycle-toolkit~=0.4.1",
"astrapy>=2.1.0,<3.0.0",
"aioboto3>=15.2.0,<16.0.0"

View File

@@ -11,6 +11,10 @@
# production
/build
# storybook
/storybook-static
*storybook.log
# misc
.DS_Store
.env.local

5
src/frontend/.storybook/css.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
// Type declarations for CSS imports in Storybook
declare module "*.css" {
const content: Record<string, string>;
export default content;
}

View File

@@ -0,0 +1,41 @@
// This file has been automatically migrated to valid ESM format by Storybook.
import { fileURLToPath } from "node:url";
import type { StorybookConfig } from "@storybook/react-vite";
import path, { dirname } from "path";
import { mergeConfig } from "vite";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const config: StorybookConfig = {
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
addons: ["@storybook/addon-links", "@storybook/addon-docs"],
framework: {
name: "@storybook/react-vite",
options: {},
},
async viteFinal(config) {
return mergeConfig(config, {
resolve: {
alias: {
"@": path.resolve(__dirname, "../src"),
},
},
});
},
typescript: {
check: false,
reactDocgen: "react-docgen-typescript",
reactDocgenTypescriptOptions: {
shouldExtractLiteralValuesFromEnum: true,
propFilter: (prop) => {
if (prop.parent) {
return !prop.parent.fileName.includes("node_modules");
}
return true;
},
},
},
};
export default config;

View File

@@ -0,0 +1,87 @@
import type { Preview } from "@storybook/react-vite";
import * as React from "react";
import { useEffect } from "react";
// Import all CSS files to match the app's styling
import "../src/style/classes.css";
import "../src/style/index.css";
import "../src/App.css";
import "../src/style/applies.css";
import { TooltipProvider } from "../src/components/ui/tooltip";
import { useDarkStore } from "../src/stores/darkStore";
// Global decorator to provide TooltipProvider for all stories
const withTooltipProvider = (Story: React.ComponentType) => {
const TooltipWrapper = (): React.ReactElement => {
const storyElement = React.createElement(Story);
return React.createElement(TooltipProvider, {
skipDelayDuration: 0,
children: storyElement,
});
};
return React.createElement(TooltipWrapper);
};
// Global decorator to handle dark mode for all stories
const withTheme = (
Story: React.ComponentType,
context: {
args?: { darkMode?: boolean };
initialArgs?: { darkMode?: boolean };
parameters?: { darkMode?: boolean };
globals?: { theme?: string };
},
) => {
const ThemeWrapper = (): React.ReactElement => {
// Check for dark mode in args, parameters, or globals
const dark =
context.args?.darkMode === true ||
context.initialArgs?.darkMode === true ||
context.parameters?.darkMode === true ||
context.globals?.theme === "dark";
useEffect(() => {
const bodyElement = document.getElementById("body") || document.body;
const htmlElement = document.documentElement;
if (dark) {
bodyElement.classList.add("dark");
htmlElement.classList.add("dark");
useDarkStore.setState({ dark: true });
} else {
bodyElement.classList.remove("dark");
htmlElement.classList.remove("dark");
useDarkStore.setState({ dark: false });
}
}, [dark]);
// Sync immediately on render
const bodyElement = document.getElementById("body") || document.body;
const htmlElement = document.documentElement;
if (dark) {
bodyElement.classList.add("dark");
htmlElement.classList.add("dark");
} else {
bodyElement.classList.remove("dark");
htmlElement.classList.remove("dark");
}
return React.createElement(Story);
};
return React.createElement(ThemeWrapper);
};
const preview: Preview = {
decorators: [withTooltipProvider, withTheme],
parameters: {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
};
export default preview;

File diff suppressed because it is too large Load Diff

View File

@@ -100,10 +100,16 @@
"serve": "vite preview",
"format": "npx @biomejs/biome format --write",
"lint": "npx @biomejs/biome lint",
"lint:changed": "bash scripts/lint-changed.sh",
"lint:types": "npx @biomejs/biome lint --diagnostic-level=error",
"lint:types:staged": "npx @biomejs/biome lint --staged --diagnostic-level=error",
"lint:types:changed": "bash scripts/lint-changed.sh",
"check-format": "npx @biomejs/biome check",
"type-check": "tsc --noEmit --pretty --project tsconfig.json && vite"
"type-check": "tsc --noEmit --pretty --project tsconfig.json && vite",
"storybook": "storybook dev -p 6006",
"storybook:network": "storybook dev -p 6006 --host 0.0.0.0",
"build-storybook": "storybook build",
"chromatic": "chromatic --project-token=${CHROMATIC_PROJECT_TOKEN}"
},
"browserslist": {
"production": [
@@ -121,6 +127,10 @@
"devDependencies": {
"@biomejs/biome": "2.1.1",
"@jest/types": "^30.0.1",
"@storybook/addon-docs": "^10.1.0",
"@storybook/addon-links": "^10.1.0",
"@storybook/react": "^10.1.0",
"@storybook/react-vite": "^10.1.0",
"@modelcontextprotocol/server-everything": "^0.6.2",
"@playwright/test": "^1.56.0",
"@swc/cli": "^0.5.2",
@@ -142,6 +152,7 @@
"jest-environment-jsdom": "^30.0.2",
"jest-junit": "^16.0.0",
"postcss": "^8.4.38",
"storybook": "^10.1.0",
"tailwindcss": "^3.4.4",
"tailwindcss-dotted-background": "^1.1.0",
"ts-jest": "^29.4.0",

View File

@@ -0,0 +1,336 @@
import type { Meta, StoryContext, StoryObj } from "@storybook/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useEffect, useMemo, useRef } from "react";
import { MemoryRouter } from "react-router-dom";
import { v5 as uuidv5 } from "uuid";
import { useDarkStore } from "@/stores/darkStore";
import useFlowStore from "@/stores/flowStore";
import useFlowsManagerStore from "@/stores/flowsManagerStore";
import { useMessagesStore } from "@/stores/messagesStore";
import { useUtilityStore } from "@/stores/utilityStore";
import type { AllNodeType, FlowType } from "@/types/flow";
import type { FlowStoreType } from "@/types/zustand/flow";
import PlaygroundPage from "./index";
type StoryArgs = { darkMode?: boolean; flowId?: string };
const withDarkMode = (
Story: React.ComponentType,
context: StoryContext<StoryArgs>,
) => {
const DarkModeWrapper = () => {
const dark = context.args?.darkMode ?? false;
const prevDark = useRef<boolean | undefined>(undefined);
useEffect(() => {
// Only update if dark mode actually changed
if (prevDark.current === dark) return;
prevDark.current = dark;
const body = document.getElementById("body") || document.body;
const currentDark = useDarkStore.getState().dark;
if (dark && !currentDark) {
body.classList.add("dark");
useDarkStore.setState({ dark: true });
} else if (!dark && currentDark) {
body.classList.remove("dark");
useDarkStore.setState({ dark: false });
}
}, [dark]);
return <Story />;
};
return <DarkModeWrapper />;
};
const withRouter = (
Story: React.ComponentType,
context: StoryContext<StoryArgs>,
) => {
const flowId = context.args?.flowId || "test-flow-id";
return (
<MemoryRouter initialEntries={[`/playground/${flowId}`]}>
<Story />
</MemoryRouter>
);
};
const withQueryClient = (
Story: React.ComponentType,
context: StoryContext<StoryArgs>,
) => {
const QueryClientWrapper = () => {
const flowId = context.args?.flowId || "test-flow-id";
const queryClient = useMemo(
() =>
new QueryClient({
defaultOptions: {
queries: { retry: false, staleTime: Infinity },
mutations: { retry: false },
},
}),
[],
);
useEffect(() => {
const clientId = "test-client-id";
const computedFlowId = uuidv5(`${clientId}_${flowId}`, uuidv5.DNS);
queryClient.setQueryData(
[
"useGetMessagesQuery",
{
mode: "union",
id: computedFlowId,
params: { session_id: computedFlowId },
},
],
{ data: { messages: [] } },
);
queryClient.setQueryData(
["useGetSessionsFromFlowQuery", { id: flowId }],
{
data: { sessions: [computedFlowId] },
},
);
}, [flowId, queryClient]);
return (
<QueryClientProvider client={queryClient}>
<Story />
</QueryClientProvider>
);
};
return <QueryClientWrapper />;
};
const withHidePublishElements = (Story: React.ComponentType) => {
const HideElementsWrapper = () => {
useEffect(() => {
const style = document.createElement("style");
style.id = "hide-publish-elements";
style.textContent = `
.absolute.bottom-2.left-0.flex.w-full.flex-col.gap-8 { display: none !important; }
.absolute.bottom-6.left-4.hidden.transition-all.md\\:block { display: none !important; }
div.flex.h-full.w-full.flex-col.justify-between.px-4.pb-4.pt-2 > div.flex.h-10.shrink-0.items-center.text-base.font-semibold > div.truncate.text-center.font-semibold {
position: absolute !important; left: 50% !important; transform: translateX(-50%) !important;
}
`;
document.head.appendChild(style);
return () => {
const existingStyle = document.getElementById("hide-publish-elements");
if (existingStyle) document.head.removeChild(existingStyle);
};
}, []);
return <Story />;
};
return <HideElementsWrapper />;
};
const withPlaygroundPageSetup = (
Story: React.ComponentType,
context: StoryContext<StoryArgs>,
) => {
const PlaygroundPageWrapper = () => {
const flowId = context.args?.flowId || "test-flow-id";
const setupDone = useRef<string | null>(null);
useEffect(() => {
// Prevent re-setup if already done for this flowId
if (setupDone.current === flowId) return;
setupDone.current = flowId;
const mockNodes: AllNodeType[] = [
{
id: "chat-input-1",
type: "genericNode",
position: { x: 0, y: 0 },
data: {
type: "ChatInput",
id: "chat-input-1",
node: {
display_name: "Chat Input",
description: "",
documentation: "",
tool_mode: false,
frozen: false,
template: {
input_value: {
type: "str",
required: false,
placeholder: "",
list: false,
show: true,
readonly: false,
value: "",
},
},
},
},
},
{
id: "chat-output-1",
type: "genericNode",
position: { x: 0, y: 0 },
data: {
type: "ChatOutput",
id: "chat-output-1",
node: {
display_name: "Chat Output",
description: "",
documentation: "",
tool_mode: false,
frozen: false,
template: {},
},
},
},
];
const mockFlow: FlowType = {
id: flowId,
name: "Playground",
description: "A test chat flow",
data: {
nodes: mockNodes,
edges: [],
viewport: { x: 0, y: 0, zoom: 1 },
},
is_component: false,
access_type: "PUBLIC",
updated_at: new Date().toISOString(),
};
useFlowsManagerStore.setState({
currentFlowId: flowId,
currentFlow: mockFlow,
isLoading: false,
});
const mockBuildFlow: FlowStoreType["buildFlow"] = async ({
input_value,
session,
files,
startNodeId,
stopNodeId,
silent,
stream,
eventDelivery,
}) => {
if (!input_value?.trim()) return;
const realFlowId =
useFlowsManagerStore.getState().currentFlowId || flowId;
const clientId =
useUtilityStore.getState().clientId || "test-client-id";
const playgroundPage = useFlowStore.getState().playgroundPage;
const computedFlowId = playgroundPage
? uuidv5(`${clientId}_${realFlowId}`, uuidv5.DNS)
: realFlowId;
const sessionId = session || computedFlowId;
useFlowStore.setState({ isBuilding: true });
useMessagesStore.getState().addMessage({
id: `msg-user-${Date.now()}`,
text: input_value,
sender: "User",
sender_name: "User",
flow_id: computedFlowId,
session_id: sessionId,
timestamp: new Date().toISOString(),
files: files || [],
edit: false,
background_color: "",
text_color: "",
content_blocks: [],
properties: {},
});
setTimeout(() => {
useMessagesStore.getState().addMessage({
id: `msg-bot-${Date.now()}`,
text: `This is a simulated response to: "${input_value}"`,
sender: "Machine",
sender_name: "Assistant",
flow_id: computedFlowId,
session_id: sessionId,
timestamp: new Date().toISOString(),
files: [],
edit: false,
background_color: "",
text_color: "",
content_blocks: [],
properties: {},
});
useFlowStore.setState({ isBuilding: false });
}, 500);
};
useFlowStore.getState().setNodes(mockNodes);
useFlowStore.setState({
currentFlow: mockFlow,
edges: [],
isBuilding: false,
playgroundPage: true,
buildFlow: mockBuildFlow,
});
useMessagesStore.setState({ messages: [], displayLoadingMessage: false });
useUtilityStore.setState({
clientId: "test-client-id",
chatValueStore: "",
currentSessionId: flowId,
});
}, [flowId]);
return <Story />;
};
return <PlaygroundPageWrapper />;
};
const meta: Meta<typeof PlaygroundPage> = {
title: "Pages/PlaygroundPage",
component: PlaygroundPage,
decorators: [
withQueryClient,
(Story) => (
<div style={{ height: "100vh", width: "100vw" }}>
<Story />
</div>
),
withHidePublishElements,
withPlaygroundPageSetup,
withRouter,
withDarkMode,
],
parameters: {
layout: "fullscreen",
docs: {
description: {
component:
"The PlaygroundPage component displays a chat interface for testing flows.",
},
},
},
tags: ["autodocs"],
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Playground: Story = {
argTypes: {
darkMode: {
control: "boolean",
description: "Toggle dark mode",
table: { category: "Theme" },
},
flowId: {
control: "text",
description: "Flow ID for the playground",
table: { category: "Configuration" },
},
},
args: {
flowId: "test-flow-id",
darkMode: false,
} as StoryArgs & Record<string, unknown>,
};

View File

@@ -10,7 +10,8 @@
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"moduleResolution": "bundler",
"types": ["@storybook/react"],
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,