diff --git a/.rat-excludes b/.rat-excludes
index 135f96cb3ba3..44cf26ac6a3a 100644
--- a/.rat-excludes
+++ b/.rat-excludes
@@ -75,6 +75,7 @@ logos/*
erd.puml
erd.svg
intro_header.txt
+TODO.md
# for LLMs
llm-context.md
diff --git a/AGENTS.md b/AGENTS.md
index 6e1efb4a1bd8..16f085e4e86f 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -101,6 +101,30 @@ superset/
- **UPDATING.md**: Add breaking changes here
- **Docstrings**: Required for new functions/classes
+## Developer Portal: Storybook-to-MDX Documentation
+
+The Developer Portal auto-generates MDX documentation from Storybook stories. **Stories are the single source of truth.**
+
+### Core Philosophy
+- **Fix issues in the STORY, not the generator** - When something doesn't render correctly, update the story file first
+- **Generator should be lightweight** - It extracts and passes through data; avoid special cases
+- **Stories define everything** - Props, controls, galleries, examples all come from story metadata
+
+### Story Requirements for Docs Generation
+- Use `export default { title: '...' }` (inline), not `const meta = ...; export default meta;`
+- Name interactive stories `Interactive${ComponentName}` (e.g., `InteractiveButton`)
+- Define `args` for default prop values
+- Define `argTypes` at the story level (not meta level) with control types and descriptions
+- Use `parameters.docs.gallery` for size×style variant grids
+- Use `parameters.docs.sampleChildren` for components that need children
+- Use `parameters.docs.liveExample` for custom live code blocks
+- Use `parameters.docs.staticProps` for complex object props that can't be parsed inline
+
+### Generator Location
+- Script: `docs/scripts/generate-superset-components.mjs`
+- Wrapper: `docs/src/components/StorybookWrapper.jsx`
+- Output: `docs/developer_portal/components/`
+
## Architecture Patterns
### Security & Features
diff --git a/docs/.claude/instructions.md b/docs/.claude/instructions.md
new file mode 100644
index 000000000000..54db2f6f121c
--- /dev/null
+++ b/docs/.claude/instructions.md
@@ -0,0 +1,115 @@
+# Developer Portal Documentation Instructions
+
+## Core Principle: Stories Are the Single Source of Truth
+
+When working on the Storybook-to-MDX documentation system:
+
+**ALWAYS fix the story first. NEVER add workarounds to the generator.**
+
+## Why This Matters
+
+The generator (`scripts/generate-superset-components.mjs`) should be lightweight - it extracts data from stories and passes it through. When you add special cases to the generator:
+- It becomes harder to maintain
+- Stories diverge from their docs representation
+- Future stories need to know about generator quirks
+
+When you fix stories to match the expected patterns:
+- Stories work identically in Storybook and Docs
+- The generator stays simple and predictable
+- Patterns are consistent and learnable
+
+## Story Patterns for Docs Generation
+
+### Required Structure
+```tsx
+// Use inline export default (NOT const meta = ...; export default meta)
+export default {
+ title: 'Components/MyComponent',
+ component: MyComponent,
+};
+
+// Name interactive stories with Interactive prefix
+export const InteractiveMyComponent: Story = {
+ args: {
+ // Default prop values
+ },
+ argTypes: {
+ // Control definitions - MUST be at story level, not meta level
+ propName: {
+ control: { type: 'select' },
+ options: ['a', 'b', 'c'],
+ description: 'What this prop does',
+ },
+ },
+};
+```
+
+### For Components with Variants (size × style grids)
+```tsx
+const sizes = ['small', 'medium', 'large'];
+const variants = ['primary', 'secondary', 'danger'];
+
+InteractiveButton.parameters = {
+ docs: {
+ gallery: {
+ component: 'Button',
+ sizes,
+ styles: variants,
+ sizeProp: 'size',
+ styleProp: 'variant',
+ },
+ },
+};
+```
+
+### For Components Requiring Children
+```tsx
+InteractiveIconTooltip.parameters = {
+ docs: {
+ // Component descriptors with dot notation for nested components
+ sampleChildren: [{ component: 'Icons.InfoCircleOutlined', props: { iconSize: 'l' } }],
+ },
+};
+```
+
+### For Custom Live Code Examples
+```tsx
+InteractiveMyComponent.parameters = {
+ docs: {
+ liveExample: `function Demo() {
+ return Content;
+}`,
+ },
+};
+```
+
+### For Complex Props (objects, arrays)
+```tsx
+InteractiveMenu.parameters = {
+ docs: {
+ staticProps: {
+ items: [
+ { key: '1', label: 'Item 1' },
+ { key: '2', label: 'Item 2' },
+ ],
+ },
+ },
+};
+```
+
+## Common Issues and How to Fix Them (in the Story)
+
+| Issue | Wrong Approach | Right Approach |
+|-------|---------------|----------------|
+| Component not generated | Add pattern to generator | Change story to use inline `export default` |
+| Control shows as text instead of select | Add special case in generator | Add `argTypes` with `control: { type: 'select' }` |
+| Missing children/content | Modify StorybookWrapper | Add `parameters.docs.sampleChildren` |
+| Gallery not showing | Add to generator output | Add `parameters.docs.gallery` config |
+| Wrong live example | Hardcode in generator | Add `parameters.docs.liveExample` |
+
+## Files
+
+- **Generator**: `docs/scripts/generate-superset-components.mjs`
+- **Wrapper**: `docs/src/components/StorybookWrapper.jsx`
+- **Output**: `docs/developer_portal/components/`
+- **Stories**: `superset-frontend/packages/superset-ui-core/src/components/*/`
diff --git a/docs/.gitignore b/docs/.gitignore
index 80249d7797be..37df51ce524a 100644
--- a/docs/.gitignore
+++ b/docs/.gitignore
@@ -35,5 +35,12 @@ docs/databases/
# Source of truth is static/resources/openapi.json
docs/api/
+# Generated component documentation MDX files (regenerated at build time)
+# Source of truth is Storybook stories in superset-frontend/packages/superset-ui-core/src/components/
+developer_portal/components/
+
+# Generated extension component documentation (regenerated at build time)
+developer_portal/extensions/components/
+
# Note: src/data/databases.json is COMMITTED (not ignored) to preserve feature diagnostics
# that require Flask context to generate. Update it locally with: npm run gen-db-docs
diff --git a/docs/babel.config.js b/docs/babel.config.js
index e1e4c0bc50c2..61032e678a4d 100644
--- a/docs/babel.config.js
+++ b/docs/babel.config.js
@@ -19,5 +19,14 @@
*/
module.exports = {
- presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
+ presets: [
+ [
+ require.resolve('@docusaurus/core/lib/babel/preset'),
+ {
+ runtime: 'automatic',
+ importSource: '@emotion/react',
+ },
+ ],
+ ],
+ plugins: ['@emotion/babel-plugin'],
};
diff --git a/docs/developer_portal/contributing/howtos.md b/docs/developer_portal/contributing/howtos.md
index 8468b8ca2054..cb52bd5b4c99 100644
--- a/docs/developer_portal/contributing/howtos.md
+++ b/docs/developer_portal/contributing/howtos.md
@@ -258,19 +258,7 @@ For debugging the Flask backend:
### Storybook
-Storybook is used for developing and testing UI components in isolation:
-
-```bash
-cd superset-frontend
-
-# Start Storybook
-npm run storybook
-
-# Build static Storybook
-npm run build-storybook
-```
-
-Access Storybook at http://localhost:6006
+See the dedicated [Storybook documentation](../testing/storybook) for information on running Storybook locally and adding new stories.
## Contributing Translations
diff --git a/docs/developer_portal/extensions/components/alert.mdx b/docs/developer_portal/extensions/components/alert.mdx
deleted file mode 100644
index f83234a01b53..000000000000
--- a/docs/developer_portal/extensions/components/alert.mdx
+++ /dev/null
@@ -1,131 +0,0 @@
----
-title: Alert
-sidebar_label: Alert
----
-
-
-
-import { StoryWithControls } from '../../../src/components/StorybookWrapper';
-import { Alert } from '@apache-superset/core/ui';
-
-# Alert
-
-Alert component for displaying important messages to users. Wraps Ant Design Alert with sensible defaults and improved accessibility.
-
-## Live Example
-
-
-
-## Try It
-
-Edit the code below to experiment with the component:
-
-```tsx live
-function Demo() {
- return (
-
- );
-}
-```
-
-## Props
-
-| Prop | Type | Default | Description |
-|------|------|---------|-------------|
-| `closable` | `boolean` | `true` | Whether the Alert can be closed with a close button. |
-| `type` | `string` | `"info"` | Type of the alert (e.g., info, error, warning, success). |
-| `message` | `string` | `"This is a sample alert message."` | Message |
-| `description` | `string` | `"Sample description for additional context."` | Description |
-| `showIcon` | `boolean` | `true` | Whether to display an icon in the Alert. |
-
-## Usage in Extensions
-
-This component is available in the `@apache-superset/core/ui` package, which is automatically available to Superset extensions.
-
-```tsx
-import { Alert } from '@apache-superset/core/ui';
-
-function MyExtension() {
- return (
-
- );
-}
-```
-
-## Source Links
-
-- [Story file](https://github.com/apache/superset/blob/master/superset-frontend/packages/superset-core/src/ui/components/Alert/Alert.stories.tsx)
-- [Component source](https://github.com/apache/superset/blob/master/superset-frontend/packages/superset-core/src/ui/components/Alert/index.tsx)
-
----
-
-*This page was auto-generated from the component's Storybook story.*
diff --git a/docs/developer_portal/extensions/components/index.mdx b/docs/developer_portal/extensions/components/index.mdx
deleted file mode 100644
index e40b4126f7f7..000000000000
--- a/docs/developer_portal/extensions/components/index.mdx
+++ /dev/null
@@ -1,93 +0,0 @@
----
-title: Extension Components
-sidebar_label: Overview
-sidebar_position: 1
----
-
-
-
-# Extension Components
-
-These UI components are available to Superset extension developers through the `@apache-superset/core/ui` package. They provide a consistent look and feel with the rest of Superset and are designed to be used in extension panels, views, and other UI elements.
-
-## Available Components
-
-- [Alert](./alert)
-
-## Usage
-
-All components are exported from the `@apache-superset/core/ui` package:
-
-```tsx
-import { Alert } from '@apache-superset/core/ui';
-
-export function MyExtensionPanel() {
- return (
-
- Welcome to my extension!
-
- );
-}
-```
-
-## Adding New Components
-
-Components in `@apache-superset/core/ui` are automatically documented here. To add a new extension component:
-
-1. Add the component to `superset-frontend/packages/superset-core/src/ui/components/`
-2. Export it from `superset-frontend/packages/superset-core/src/ui/components/index.ts`
-3. Create a Storybook story with an `Interactive` export:
-
-```tsx
-export default {
- title: 'Extension Components/MyComponent',
- component: MyComponent,
- parameters: {
- docs: {
- description: {
- component: 'Description of the component...',
- },
- },
- },
-};
-
-export const InteractiveMyComponent = (args) => ;
-
-InteractiveMyComponent.args = {
- variant: 'primary',
- disabled: false,
-};
-
-InteractiveMyComponent.argTypes = {
- variant: {
- control: { type: 'select' },
- options: ['primary', 'secondary'],
- },
- disabled: {
- control: { type: 'boolean' },
- },
-};
-```
-
-4. Run `yarn start` in `docs/` - the page generates automatically!
-
-## Interactive Documentation
-
-For interactive examples with controls, visit the [Storybook](/storybook/?path=/docs/extension-components--docs).
diff --git a/docs/developer_portal/sidebars.js b/docs/developer_portal/sidebars.js
index 3e81c7e5cc59..7c376be945e7 100644
--- a/docs/developer_portal/sidebars.js
+++ b/docs/developer_portal/sidebars.js
@@ -26,6 +26,9 @@ module.exports = {
collapsed: true,
items: [
'contributing/overview',
+ 'guidelines/design-guidelines',
+ 'guidelines/frontend-style-guidelines',
+ 'guidelines/backend-style-guidelines',
],
},
{
@@ -61,5 +64,20 @@ module.exports = {
'testing/overview',
],
},
+ {
+ type: 'category',
+ label: 'UI Components',
+ collapsed: true,
+ link: {
+ type: 'doc',
+ id: 'components/index',
+ },
+ items: [
+ {
+ type: 'autogenerated',
+ dirName: 'components',
+ },
+ ],
+ },
],
};
diff --git a/docs/developer_portal/testing/storybook.md b/docs/developer_portal/testing/storybook.md
new file mode 100644
index 000000000000..0e190220f159
--- /dev/null
+++ b/docs/developer_portal/testing/storybook.md
@@ -0,0 +1,114 @@
+---
+title: Storybook
+sidebar_position: 5
+---
+
+
+
+# Storybook
+
+Superset uses [Storybook](https://storybook.js.org/) for developing and testing UI components in isolation. Storybook provides a sandbox to build components independently, outside of the main application.
+
+## Public Storybook
+
+A public Storybook with components from the `master` branch is available at:
+
+**[apache-superset.github.io/superset-ui](https://apache-superset.github.io/superset-ui/?path=/story/*)**
+
+## Running Locally
+
+### Main Superset Storybook
+
+To run the main Superset Storybook locally:
+
+```bash
+cd superset-frontend
+
+# Start Storybook (opens at http://localhost:6006)
+npm run storybook
+
+# Build static Storybook
+npm run build-storybook
+```
+
+### @superset-ui Package Storybook
+
+The `@superset-ui` packages have a separate Storybook for component library development:
+
+```bash
+cd superset-frontend
+
+# Install dependencies and bootstrap packages
+npm ci && npm run bootstrap
+
+# Start the @superset-ui Storybook (opens at http://localhost:9001)
+cd packages/superset-ui-demo
+npm run storybook
+```
+
+## Adding Stories
+
+### To an Existing Package
+
+If stories already exist for the package, extend the `examples` array in the package's story file:
+
+```
+storybook/stories//index.js
+```
+
+### To a New Package
+
+1. Add package dependencies:
+
+ ```bash
+ npm install
+ ```
+
+2. Create a story folder matching the package name:
+
+ ```bash
+ mkdir storybook/stories/superset-ui-/
+ ```
+
+3. Create an `index.js` file with the story configuration:
+
+ ```javascript
+ export default {
+ examples: [
+ {
+ storyPath: '@superset-ui/package',
+ storyName: 'My Story',
+ renderStory: () => ,
+ },
+ ],
+ };
+ ```
+
+ Use the `|` separator for nested stories:
+ ```javascript
+ storyPath: '@superset-ui/package|Category|Subcategory'
+ ```
+
+## Best Practices
+
+- **Isolate components**: Stories should render components in isolation, without application context
+- **Show variations**: Create stories for different states, sizes, and configurations
+- **Document props**: Use Storybook's controls to expose configurable props
+- **Test edge cases**: Include stories for loading states, error states, and empty states
diff --git a/docs/docusaurus.config.ts b/docs/docusaurus.config.ts
index 85a2cc9b5d37..7552c5917401 100644
--- a/docs/docusaurus.config.ts
+++ b/docs/docusaurus.config.ts
@@ -135,26 +135,26 @@ if (!versionsConfig.developer_portal.disabled && !versionsConfig.developer_porta
{
type: 'doc',
docsPluginId: 'developer_portal',
- docId: 'extensions/overview',
- label: 'Extensions',
+ docId: 'contributing/overview',
+ label: 'Contributing',
},
{
type: 'doc',
docsPluginId: 'developer_portal',
- docId: 'testing/overview',
- label: 'Testing',
+ docId: 'extensions/overview',
+ label: 'Extensions',
},
{
type: 'doc',
docsPluginId: 'developer_portal',
- docId: 'guidelines/design-guidelines',
- label: 'Guidelines',
+ docId: 'testing/overview',
+ label: 'Testing',
},
{
type: 'doc',
docsPluginId: 'developer_portal',
- docId: 'contributing/overview',
- label: 'Contributing',
+ docId: 'components/index',
+ label: 'UI Components',
},
{
label: 'API Reference',
diff --git a/docs/netlify.toml b/docs/netlify.toml
index 2ec4fa197837..fde7b6d83923 100644
--- a/docs/netlify.toml
+++ b/docs/netlify.toml
@@ -34,6 +34,8 @@
NODE_VERSION = "20"
# Yarn version
YARN_VERSION = "1.22.22"
+ # Increase heap size for webpack bundling of Superset UI components
+ NODE_OPTIONS = "--max-old-space-size=4096"
# Deploy preview settings
[context.deploy-preview]
diff --git a/docs/package.json b/docs/package.json
index a511a7f81542..7f5ef41ffcf0 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -6,9 +6,9 @@
"scripts": {
"docusaurus": "docusaurus",
"_init": "cat src/intro_header.txt ../README.md > docs/intro.md",
- "start": "yarn run _init && yarn run generate:extension-components && yarn run generate:database-docs && yarn run generate:api-docs && NODE_ENV=development docusaurus start",
+ "start": "yarn run _init && yarn run generate:all && NODE_ENV=development docusaurus start",
"stop": "pkill -f 'docusaurus start' || pkill -f 'docusaurus serve' || echo 'No docusaurus server running'",
- "build": "yarn run _init && yarn run generate:extension-components && yarn run generate:database-docs && yarn run generate:api-docs && DEBUG=docusaurus:* docusaurus build",
+ "build": "yarn run _init && yarn run generate:all && DEBUG=docusaurus:* docusaurus build",
"generate:api-docs": "python3 scripts/fix-openapi-spec.py && docusaurus gen-api-docs superset && node scripts/convert-api-sidebar.mjs && node scripts/generate-api-index.mjs && node scripts/generate-api-tag-pages.mjs",
"clean:api-docs": "docusaurus clean-api-docs superset",
"swizzle": "docusaurus swizzle",
@@ -17,10 +17,12 @@
"serve": "yarn run _init && docusaurus serve",
"write-translations": "docusaurus write-translations",
"write-heading-ids": "docusaurus write-heading-ids",
- "typecheck": "yarn run generate:extension-components && yarn run generate:database-docs && tsc",
+ "typecheck": "yarn run generate:all && tsc",
"generate:extension-components": "node scripts/generate-extension-components.mjs",
+ "generate:superset-components": "node scripts/generate-superset-components.mjs",
"generate:database-docs": "node scripts/generate-database-docs.mjs",
"gen-db-docs": "node scripts/generate-database-docs.mjs",
+ "generate:all": "yarn run generate:extension-components && yarn run generate:superset-components && yarn run generate:database-docs && yarn run generate:api-docs",
"lint:db-metadata": "python3 ../superset/db_engine_specs/lint_metadata.py",
"lint:db-metadata:report": "python3 ../superset/db_engine_specs/lint_metadata.py --markdown -o ../superset/db_engine_specs/METADATA_STATUS.md",
"update:readme-db-logos": "node scripts/generate-database-docs.mjs --update-readme",
@@ -36,14 +38,20 @@
},
"dependencies": {
"@ant-design/icons": "^6.1.0",
+ "@babel/core": "^7.26.0",
+ "@babel/preset-react": "^7.26.3",
+ "@babel/preset-typescript": "^7.26.0",
"@docusaurus/core": "3.9.2",
"@docusaurus/plugin-client-redirects": "3.9.2",
"@docusaurus/preset-classic": "3.9.2",
"@docusaurus/theme-live-codeblock": "^3.9.2",
"@docusaurus/theme-mermaid": "^3.9.2",
+ "@emotion/babel-plugin": "^11.13.5",
"@emotion/core": "^11.0.0",
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.14.1",
+ "@fontsource/fira-code": "^5.2.7",
+ "@fontsource/inter": "^5.2.8",
"@mdx-js/react": "^3.1.1",
"@saucelabs/theme-github-codeblock": "^0.3.0",
"@storybook/addon-docs": "^8.6.15",
@@ -59,6 +67,7 @@
"@storybook/theming": "^8.6.11",
"@superset-ui/core": "^0.20.4",
"antd": "^6.2.2",
+ "babel-loader": "^9.2.1",
"caniuse-lite": "^1.0.30001766",
"docusaurus-plugin-less": "^2.0.2",
"docusaurus-plugin-openapi-docs": "^4.6.0",
@@ -72,7 +81,9 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-github-btn": "^1.4.0",
+ "react-resize-detector": "7.1.2",
"react-svg-pan-zoom": "^3.13.1",
+ "react-table": "^7.8.0",
"remark-import-partial": "^0.0.2",
"reselect": "^5.1.1",
"storybook": "^8.6.15",
diff --git a/docs/scripts/generate-superset-components.mjs b/docs/scripts/generate-superset-components.mjs
new file mode 100644
index 000000000000..a85a49bef0a8
--- /dev/null
+++ b/docs/scripts/generate-superset-components.mjs
@@ -0,0 +1,1415 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * ============================================================================
+ * PHILOSOPHY: STORIES ARE THE SINGLE SOURCE OF TRUTH
+ * ============================================================================
+ *
+ * When something doesn't render correctly in the docs, FIX THE STORY FIRST.
+ * Do NOT add special cases or workarounds to this generator.
+ *
+ * This generator should be as lightweight as possible - it extracts data from
+ * stories and passes it through to MDX. All configuration belongs in stories:
+ *
+ * - Use `export default { title: '...' }` (inline export, not variable)
+ * - Name stories `Interactive${ComponentName}` for docs generation
+ * - Define `args` and `argTypes` at the story level (not meta level)
+ * - Use `parameters.docs.gallery` for variant grids
+ * - Use `parameters.docs.sampleChildren` for components needing children
+ * - Use `parameters.docs.liveExample` for custom code examples
+ * - Use `parameters.docs.staticProps` for complex props
+ *
+ * If a story doesn't work with this generator, fix the story to match the
+ * expected patterns rather than adding complexity here.
+ * ============================================================================
+ */
+
+/**
+ * This script scans for ALL Storybook stories and generates MDX documentation
+ * pages for the "Superset Components" section of the developer portal.
+ *
+ * Supports multiple source directories with different import paths and categories.
+ *
+ * Usage: node scripts/generate-superset-components.mjs
+ */
+
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+const ROOT_DIR = path.resolve(__dirname, '../..');
+const DOCS_DIR = path.resolve(__dirname, '..');
+const OUTPUT_DIR = path.join(DOCS_DIR, 'developer_portal/components');
+const FRONTEND_DIR = path.join(ROOT_DIR, 'superset-frontend');
+
+// Source configurations with import paths and categories
+const SOURCES = [
+ {
+ name: 'UI Core Components',
+ path: 'packages/superset-ui-core/src/components',
+ importPrefix: '@superset/components',
+ docImportPrefix: '@superset-ui/core/components',
+ category: 'ui',
+ enabled: true,
+ // Components that require complex function props or aren't exported properly
+ skipComponents: new Set([
+ // Complex function props (require callbacks, async data, or render props)
+ 'AsyncSelect', 'ConfirmStatusChange', 'CronPicker', 'LabeledErrorBoundInput',
+ 'AsyncAceEditor', 'AsyncEsmComponent', 'TimezoneSelector',
+ // Not exported from @superset/components index or have export mismatches
+ 'ActionCell', 'BooleanCell', 'ButtonCell', 'NullCell', 'NumericCell', 'TimeCell',
+ 'CertifiedBadgeWithTooltip', 'CodeSyntaxHighlighter', 'DynamicTooltip',
+ 'PopoverDropdown', 'PopoverSection', 'WarningIconWithTooltip', 'RefreshLabel',
+ // Components with complex nested props (JSX children, overlay, items arrays)
+ 'Dropdown', 'DropdownButton',
+ ]),
+ },
+ {
+ name: 'App Components',
+ path: 'src/components',
+ importPrefix: 'src/components',
+ docImportPrefix: 'src/components',
+ category: 'app',
+ enabled: false, // Requires app context (Redux, routing, etc.)
+ skipComponents: new Set([]),
+ },
+ {
+ name: 'Dashboard Components',
+ path: 'src/dashboard/components',
+ importPrefix: 'src/dashboard/components',
+ docImportPrefix: 'src/dashboard/components',
+ category: 'dashboard',
+ enabled: false, // Requires app context
+ skipComponents: new Set([]),
+ },
+ {
+ name: 'Explore Components',
+ path: 'src/explore/components',
+ importPrefix: 'src/explore/components',
+ docImportPrefix: 'src/explore/components',
+ category: 'explore',
+ enabled: false, // Requires app context
+ skipComponents: new Set([]),
+ },
+ {
+ name: 'Feature Components',
+ path: 'src/features',
+ importPrefix: 'src/features',
+ docImportPrefix: 'src/features',
+ category: 'features',
+ enabled: false, // Requires app context
+ skipComponents: new Set([]),
+ },
+ {
+ name: 'Filter Components',
+ path: 'src/filters/components',
+ importPrefix: 'src/filters/components',
+ docImportPrefix: 'src/filters/components',
+ category: 'filters',
+ enabled: false, // Requires app context
+ skipComponents: new Set([]),
+ },
+ {
+ name: 'Chart Plugins',
+ path: 'packages/superset-ui-demo/storybook/stories/plugins',
+ importPrefix: '@superset-ui/demo',
+ docImportPrefix: '@superset-ui/demo',
+ category: 'chart-plugins',
+ enabled: false, // Requires chart infrastructure
+ skipComponents: new Set([]),
+ },
+ {
+ name: 'Core Packages',
+ path: 'packages/superset-ui-demo/storybook/stories/superset-ui-chart',
+ importPrefix: '@superset-ui/core',
+ docImportPrefix: '@superset-ui/core',
+ category: 'core-packages',
+ enabled: false, // Requires specific setup
+ skipComponents: new Set([]),
+ },
+];
+
+// Category mapping from story title prefixes to output directories
+const CATEGORY_MAP = {
+ 'Components/': 'ui',
+ 'Design System/': 'design-system',
+ 'Chart Plugins/': 'chart-plugins',
+ 'Legacy Chart Plugins/': 'legacy-charts',
+ 'Core Packages/': 'core-packages',
+ 'Others/': 'utilities',
+ 'Extension Components/': 'extension', // Skip - handled by other script
+ 'Superset App/': 'app',
+};
+
+// Documentation-only stories to skip (not actual components)
+const SKIP_STORIES = [
+ 'Introduction', // Design System intro page
+ 'Overview', // Category overview pages
+ 'Examples', // Example collections
+ 'DesignSystem', // Meta design system page
+ 'MetadataBarOverview', // Overview page
+ 'TableOverview', // Overview page
+ 'Filter Plugins', // Collection story, not a component
+];
+
+
+/**
+ * Recursively find all story files in a directory
+ */
+function walkDir(dir, files = []) {
+ if (!fs.existsSync(dir)) return files;
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
+ for (const entry of entries) {
+ const fullPath = path.join(dir, entry.name);
+ if (entry.isDirectory()) {
+ walkDir(fullPath, files);
+ } else if (entry.name.endsWith('.stories.tsx') || entry.name.endsWith('.stories.ts')) {
+ files.push(fullPath);
+ }
+ }
+ return files;
+}
+
+/**
+ * Find all story files from enabled sources
+ */
+function findEnabledStoryFiles() {
+ const files = [];
+ for (const source of SOURCES.filter(s => s.enabled)) {
+ const dir = path.join(FRONTEND_DIR, source.path);
+ const sourceFiles = walkDir(dir, []);
+ // Attach source config to each file
+ for (const file of sourceFiles) {
+ files.push({ file, source });
+ }
+ }
+ return files;
+}
+
+/**
+ * Find all story files from disabled sources (for tracking)
+ */
+function findDisabledStoryFiles() {
+ const files = [];
+ for (const source of SOURCES.filter(s => !s.enabled)) {
+ const dir = path.join(FRONTEND_DIR, source.path);
+ walkDir(dir, files);
+ }
+ return files;
+}
+
+/**
+ * Parse a story file and extract metadata
+ */
+function parseStoryFile(filePath, sourceConfig) {
+ const content = fs.readFileSync(filePath, 'utf-8');
+
+ // Extract title from story meta (in export default block, not from data objects)
+ // Look for title in the export default section, which typically starts with "export default {"
+ const metaMatch = content.match(/export\s+default\s*\{[\s\S]*?title:\s*['"]([^'"]+)['"]/);
+ const title = metaMatch ? metaMatch[1] : null;
+
+ if (!title) return null;
+
+ // Extract component name (last part of title path)
+ const titleParts = title.split('/');
+ const componentName = titleParts.pop();
+
+ // Skip documentation-only stories
+ if (SKIP_STORIES.includes(componentName)) {
+ return null;
+ }
+
+ // Skip components in the source's skip list
+ if (sourceConfig.skipComponents.has(componentName)) {
+ return null;
+ }
+
+ // Determine category - use source's default category unless title has a specific prefix
+ let category = sourceConfig.category;
+ for (const [prefix, cat] of Object.entries(CATEGORY_MAP)) {
+ if (title.startsWith(prefix)) {
+ category = cat;
+ break;
+ }
+ }
+
+ // Extract description from parameters
+ let description = '';
+ const descBlockMatch = content.match(
+ /description:\s*{\s*component:\s*([\s\S]*?)\s*},?\s*}/
+ );
+ if (descBlockMatch) {
+ const descBlock = descBlockMatch[1];
+ const stringParts = [];
+ const stringMatches = descBlock.matchAll(/['"]([^'"]*)['"]/g);
+ for (const match of stringMatches) {
+ stringParts.push(match[1]);
+ }
+ description = stringParts.join('').trim();
+ }
+
+ // Extract story exports
+ const storyExports = [];
+ const exportMatches = content.matchAll(/export\s+(?:const|function)\s+(\w+)/g);
+ for (const match of exportMatches) {
+ if (match[1] !== 'default') {
+ storyExports.push(match[1]);
+ }
+ }
+
+ // Extract component import path from the story file
+ // Look for: import ComponentName from './path' (default export)
+ // or: import { ComponentName } from './path' (named export)
+ let componentImportPath = null;
+ let isDefaultExport = true;
+
+ // Try to find default import matching the component name
+ // Handles: import Component from 'path'
+ // and: import Component, { OtherExport } from 'path'
+ const defaultImportMatch = content.match(
+ new RegExp(`import\\s+${componentName}(?:\\s*,\\s*{[^}]*})?\\s+from\\s+['"]([^'"]+)['"]`)
+ );
+ if (defaultImportMatch) {
+ componentImportPath = defaultImportMatch[1];
+ isDefaultExport = true;
+ } else {
+ // Try named import
+ const namedImportMatch = content.match(
+ new RegExp(`import\\s*{[^}]*\\b${componentName}\\b[^}]*}\\s*from\\s+['"]([^'"]+)['"]`)
+ );
+ if (namedImportMatch) {
+ componentImportPath = namedImportMatch[1];
+ isDefaultExport = false;
+ }
+ }
+
+ // Calculate full import path if we found a relative import
+ // For UI core components with aliases, keep using the alias
+ let resolvedImportPath = sourceConfig.importPrefix;
+ const useAlias = sourceConfig.importPrefix.startsWith('@superset/');
+
+ if (componentImportPath && componentImportPath.startsWith('.') && !useAlias) {
+ const storyDir = path.dirname(filePath);
+ const resolvedPath = path.resolve(storyDir, componentImportPath);
+ // Get path relative to frontend root, then convert to import path
+ const frontendRelative = path.relative(FRONTEND_DIR, resolvedPath);
+ resolvedImportPath = frontendRelative.replace(/\\/g, '/');
+ } else if (!componentImportPath && !useAlias) {
+ // Fallback: assume component is in same dir as story, named same as component
+ const storyDir = path.dirname(filePath);
+ const possibleComponentPath = path.join(storyDir, componentName);
+ const frontendRelative = path.relative(FRONTEND_DIR, possibleComponentPath);
+ resolvedImportPath = frontendRelative.replace(/\\/g, '/');
+ }
+
+ return {
+ filePath,
+ title,
+ titleParts,
+ componentName,
+ category,
+ description,
+ storyExports,
+ relativePath: path.relative(ROOT_DIR, filePath),
+ sourceConfig,
+ resolvedImportPath,
+ isDefaultExport,
+ };
+}
+
+/**
+ * Parse args content and extract key-value pairs
+ * Handles strings with apostrophes correctly
+ */
+function parseArgsContent(argsContent, args) {
+ // Split into lines and process each line for simple key-value pairs
+ const lines = argsContent.split('\n');
+
+ for (let i = 0; i < lines.length; i++) {
+ const trimmed = lines[i].trim();
+ if (!trimmed || trimmed.startsWith('//')) continue;
+
+ // Match: key: value pattern at start of line
+ const propMatch = trimmed.match(/^([a-zA-Z_$][a-zA-Z0-9_$]*):\s*(.+?)[\s,]*$/);
+ // Also match key with value on the next line (e.g., prettier wrapping long strings)
+ const keyOnlyMatch = !propMatch && trimmed.match(/^([a-zA-Z_$][a-zA-Z0-9_$]*):$/);
+ if (!propMatch && !keyOnlyMatch) continue;
+
+ let key, valueStr;
+ if (propMatch) {
+ key = propMatch[1];
+ valueStr = propMatch[2];
+ } else {
+ // Value is on the next line
+ key = keyOnlyMatch[1];
+ const nextLine = i + 1 < lines.length ? lines[i + 1].trim().replace(/,\s*$/, '') : '';
+ if (!nextLine) continue;
+ valueStr = nextLine;
+ i++; // Skip the next line since we consumed it
+ }
+
+ // Parse the value
+ // Double-quoted string (handles apostrophes inside)
+ const doubleQuoteMatch = valueStr.match(/^"([^"]*)"$/);
+ if (doubleQuoteMatch) {
+ args[key] = doubleQuoteMatch[1];
+ continue;
+ }
+
+ // Single-quoted string
+ const singleQuoteMatch = valueStr.match(/^'([^']*)'$/);
+ if (singleQuoteMatch) {
+ args[key] = singleQuoteMatch[1];
+ continue;
+ }
+
+ // Template literal
+ const templateMatch = valueStr.match(/^`([^`]*)`$/);
+ if (templateMatch) {
+ args[key] = templateMatch[1].replace(/\s+/g, ' ').trim();
+ continue;
+ }
+
+ // Boolean
+ if (valueStr === 'true' || valueStr === 'true,') {
+ args[key] = true;
+ continue;
+ }
+ if (valueStr === 'false' || valueStr === 'false,') {
+ args[key] = false;
+ continue;
+ }
+
+ // Number (including decimals and negative)
+ const numMatch = valueStr.match(/^(-?\d+\.?\d*),?$/);
+ if (numMatch) {
+ args[key] = Number(numMatch[1]);
+ continue;
+ }
+
+ // Skip complex values (objects, arrays, function calls, expressions)
+ }
+}
+
+/**
+ * Extract variable arrays from file content (for options references)
+ */
+function extractVariableArrays(content) {
+ const variableArrays = {};
+
+ // Pattern 1: const varName = ['a', 'b', 'c'];
+ // Also handles: export const varName: Type[] = ['a', 'b', 'c'];
+ const varMatches = content.matchAll(/(?:export\s+)?(?:const|let)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)(?::\s*[^=]+)?\s*=\s*\[([^\]]+)\]/g);
+ for (const varMatch of varMatches) {
+ const varName = varMatch[1];
+ const arrayContent = varMatch[2];
+ const values = [];
+ const valMatches = arrayContent.matchAll(/['"]([^'"]+)['"]/g);
+ for (const val of valMatches) {
+ values.push(val[1]);
+ }
+ if (values.length > 0) {
+ variableArrays[varName] = values;
+ }
+ }
+
+ // Pattern 2: const VAR = { options: [...] } - for SIZES.options, COLORS.options patterns
+ const objWithOptionsMatches = content.matchAll(/(?:const|let)\s+([A-Z][A-Z_0-9]*)\s*=\s*\{[^}]*options:\s*([a-zA-Z_$][a-zA-Z0-9_$]*)/g);
+ for (const match of objWithOptionsMatches) {
+ const objName = match[1];
+ const optionsVarName = match[2];
+ // Link the object's options to the underlying array
+ if (variableArrays[optionsVarName]) {
+ variableArrays[objName] = variableArrays[optionsVarName];
+ }
+ }
+
+ return variableArrays;
+}
+
+/**
+ * Extract a string value from content, handling quotes properly
+ */
+function extractStringValue(content, startIndex) {
+ const remaining = content.slice(startIndex).trim();
+
+ // Single-quoted string
+ if (remaining.startsWith("'")) {
+ let i = 1;
+ while (i < remaining.length) {
+ if (remaining[i] === "'" && remaining[i - 1] !== '\\') {
+ return remaining.slice(1, i);
+ }
+ i++;
+ }
+ }
+
+ // Double-quoted string
+ if (remaining.startsWith('"')) {
+ let i = 1;
+ while (i < remaining.length) {
+ if (remaining[i] === '"' && remaining[i - 1] !== '\\') {
+ return remaining.slice(1, i);
+ }
+ i++;
+ }
+ }
+
+ // Template literal
+ if (remaining.startsWith('`')) {
+ let i = 1;
+ while (i < remaining.length) {
+ if (remaining[i] === '`' && remaining[i - 1] !== '\\') {
+ return remaining.slice(1, i).replace(/\s+/g, ' ').trim();
+ }
+ i++;
+ }
+ }
+
+ return null;
+}
+
+/**
+ * Parse argTypes content and populate the argTypes object
+ */
+function parseArgTypes(argTypesContent, argTypes, fullContent) {
+ const variableArrays = extractVariableArrays(fullContent);
+
+ // Match argType definitions - find each property block
+ // Use balanced brace extraction for each property
+ const propPattern = /([a-zA-Z_$][a-zA-Z0-9_$]*):\s*\{/g;
+ let propMatch;
+
+ while ((propMatch = propPattern.exec(argTypesContent)) !== null) {
+ const propName = propMatch[1];
+ const propStartIndex = propMatch.index + propMatch[0].length - 1;
+ const propConfig = extractBalancedBraces(argTypesContent, propStartIndex);
+
+ if (!propConfig) continue;
+
+ // Initialize argTypes entry if not exists
+ if (!argTypes[propName]) {
+ argTypes[propName] = {};
+ }
+
+ // Extract description - find the position and extract properly
+ const descIndex = propConfig.indexOf('description:');
+ if (descIndex !== -1) {
+ const descValue = extractStringValue(propConfig, descIndex + 'description:'.length);
+ if (descValue) {
+ argTypes[propName].description = descValue;
+ }
+ }
+
+ // Check for inline options array
+ const optionsMatch = propConfig.match(/options:\s*\[([^\]]+)\]/);
+ if (optionsMatch) {
+ const optionsStr = optionsMatch[1];
+ const options = [];
+ const optionMatches = optionsStr.matchAll(/['"]([^'"]+)['"]/g);
+ for (const opt of optionMatches) {
+ options.push(opt[1]);
+ }
+ if (options.length > 0) {
+ argTypes[propName].type = 'select';
+ argTypes[propName].options = options;
+ }
+ } else {
+ // Check for variable reference: options: variableName or options: VAR.options
+ const varRefMatch = propConfig.match(/options:\s*([a-zA-Z_$][a-zA-Z0-9_$]*(?:\.[a-zA-Z_$][a-zA-Z0-9_$]*)?)/);
+ if (varRefMatch) {
+ const varRef = varRefMatch[1];
+ // Handle VAR.options pattern
+ const dotMatch = varRef.match(/^([a-zA-Z_$][a-zA-Z0-9_$]*)\.options$/);
+ if (dotMatch && variableArrays[dotMatch[1]]) {
+ argTypes[propName].type = 'select';
+ argTypes[propName].options = variableArrays[dotMatch[1]];
+ } else if (variableArrays[varRef]) {
+ argTypes[propName].type = 'select';
+ argTypes[propName].options = variableArrays[varRef];
+ }
+ } else {
+ // Check for ES6 shorthand: options, (same as options: options)
+ const shorthandMatch = propConfig.match(/(?:^|[,\s])options(?:[,\s]|$)/);
+ if (shorthandMatch && variableArrays['options']) {
+ argTypes[propName].type = 'select';
+ argTypes[propName].options = variableArrays['options'];
+ }
+ }
+ }
+
+ // Check for control type (radio, select, boolean, etc.)
+ // Supports both: control: 'boolean' (shorthand) and control: { type: 'boolean' } (object)
+ const controlShorthandMatch = propConfig.match(/control:\s*['"]([^'"]+)['"]/);
+ const controlObjectMatch = propConfig.match(/control:\s*\{[^}]*type:\s*['"]([^'"]+)['"]/);
+ if (controlShorthandMatch) {
+ argTypes[propName].type = controlShorthandMatch[1];
+ } else if (controlObjectMatch) {
+ argTypes[propName].type = controlObjectMatch[1];
+ }
+
+ // Clear options for non-select/radio types (the shorthand "options" detection
+ // can false-positive when the word "options" appears in description text)
+ const finalType = argTypes[propName].type;
+ if (finalType && !['select', 'radio', 'inline-radio'].includes(finalType)) {
+ delete argTypes[propName].options;
+ }
+ }
+}
+
+/**
+ * Helper to find balanced braces content
+ */
+function extractBalancedBraces(content, startIndex) {
+ let depth = 0;
+ let start = -1;
+ for (let i = startIndex; i < content.length; i++) {
+ if (content[i] === '{') {
+ if (depth === 0) start = i + 1;
+ depth++;
+ } else if (content[i] === '}') {
+ depth--;
+ if (depth === 0) {
+ return content.slice(start, i);
+ }
+ }
+ }
+ return null;
+}
+
+/**
+ * Helper to find balanced brackets content (for arrays)
+ */
+function extractBalancedBrackets(content, startIndex) {
+ let depth = 0;
+ let start = -1;
+ for (let i = startIndex; i < content.length; i++) {
+ if (content[i] === '[') {
+ if (depth === 0) start = i + 1;
+ depth++;
+ } else if (content[i] === ']') {
+ depth--;
+ if (depth === 0) {
+ return content.slice(start, i);
+ }
+ }
+ }
+ return null;
+}
+
+/**
+ * Convert camelCase prop name to human-readable label
+ * Handles acronyms properly: imgURL -> "Image URL", coverLeft -> "Cover Left"
+ */
+function propNameToLabel(name) {
+ return name
+ // Insert space before uppercase letters that follow lowercase (camelCase boundary)
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
+ // Handle common acronyms - keep them together
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
+ // Capitalize first letter
+ .replace(/^./, s => s.toUpperCase())
+ // Fix common acronyms display
+ .replace(/\bUrl\b/g, 'URL')
+ .replace(/\bImg\b/g, 'Image')
+ .replace(/\bId\b/g, 'ID');
+}
+
+/**
+ * Convert JS object literal syntax to JSON
+ * Handles: single quotes, unquoted keys, trailing commas
+ */
+function jsToJson(jsStr) {
+ try {
+ // Remove comments
+ let str = jsStr.replace(/\/\/[^\n]*/g, '').replace(/\/\*[\s\S]*?\*\//g, '');
+
+ // Replace single quotes with double quotes (but not inside already double-quoted strings)
+ str = str.replace(/'/g, '"');
+
+ // Add quotes around unquoted keys: { foo: -> { "foo":
+ str = str.replace(/([{,]\s*)([a-zA-Z_$][a-zA-Z0-9_$]*)(\s*:)/g, '$1"$2"$3');
+
+ // Remove trailing commas before } or ]
+ str = str.replace(/,(\s*[}\]])/g, '$1');
+
+ return JSON.parse(str);
+ } catch {
+ return null;
+ }
+}
+
+/**
+ * Extract docs config from story parameters
+ * Looks for: StoryName.parameters = { docs: { sampleChildren, sampleChildrenStyle, gallery, staticProps, liveExample } }
+ * Uses generic JSON parsing for inline data
+ */
+function extractDocsConfig(content, storyNames) {
+ // Extract variable arrays for gallery config (sizes, styles)
+ const variableArrays = extractVariableArrays(content);
+
+ let sampleChildren = null;
+ let sampleChildrenStyle = null;
+ let gallery = null;
+ let staticProps = null;
+ let liveExample = null;
+ let examples = null;
+ let renderComponent = null;
+ let triggerProp = null;
+ let onHideProp = null;
+
+ for (const storyName of storyNames) {
+ // Look for parameters block
+ const parametersPattern = new RegExp(`${storyName}\\.parameters\\s*=\\s*\\{`, 's');
+ const parametersMatch = content.match(parametersPattern);
+
+ if (parametersMatch) {
+ const parametersContent = extractBalancedBraces(content, parametersMatch.index + parametersMatch[0].length - 1);
+ if (parametersContent) {
+ // Extract sampleChildren - inline array using generic JSON parser
+ const sampleChildrenArrayMatch = parametersContent.match(/sampleChildren:\s*\[/);
+ if (sampleChildrenArrayMatch) {
+ const arrayStartIndex = sampleChildrenArrayMatch.index + sampleChildrenArrayMatch[0].length - 1;
+ const arrayContent = extractBalancedBrackets(parametersContent, arrayStartIndex);
+ if (arrayContent) {
+ const parsed = jsToJson('[' + arrayContent + ']');
+ if (parsed && parsed.length > 0) {
+ sampleChildren = parsed;
+ }
+ }
+ }
+
+ // Extract sampleChildrenStyle - inline object using generic JSON parser
+ const sampleChildrenStyleMatch = parametersContent.match(/sampleChildrenStyle:\s*\{/);
+ if (sampleChildrenStyleMatch) {
+ const styleContent = extractBalancedBraces(parametersContent, sampleChildrenStyleMatch.index + sampleChildrenStyleMatch[0].length - 1);
+ if (styleContent) {
+ const parsed = jsToJson('{' + styleContent + '}');
+ if (parsed) {
+ sampleChildrenStyle = parsed;
+ }
+ }
+ }
+
+ // Extract staticProps - generic JSON-like object extraction
+ const staticPropsMatch = parametersContent.match(/staticProps:\s*\{/);
+ if (staticPropsMatch) {
+ const staticPropsContent = extractBalancedBraces(parametersContent, staticPropsMatch.index + staticPropsMatch[0].length - 1);
+ if (staticPropsContent) {
+ // Try to parse as JSON (handles inline data)
+ const parsed = jsToJson('{' + staticPropsContent + '}');
+ if (parsed) {
+ staticProps = parsed;
+ }
+ }
+ }
+
+ // Extract gallery config
+ const galleryMatch = parametersContent.match(/gallery:\s*\{/);
+ if (galleryMatch) {
+ const galleryContent = extractBalancedBraces(parametersContent, galleryMatch.index + galleryMatch[0].length - 1);
+ if (galleryContent) {
+ gallery = {};
+
+ // Extract component name
+ const compMatch = galleryContent.match(/component:\s*['"]([^'"]+)['"]/);
+ if (compMatch) gallery.component = compMatch[1];
+
+ // Extract sizes - variable reference
+ const sizesVarMatch = galleryContent.match(/sizes:\s*([a-zA-Z_$][a-zA-Z0-9_$]*)/);
+ if (sizesVarMatch && variableArrays[sizesVarMatch[1]]) {
+ gallery.sizes = variableArrays[sizesVarMatch[1]];
+ }
+
+ // Extract styles - variable reference
+ const stylesVarMatch = galleryContent.match(/styles:\s*([a-zA-Z_$][a-zA-Z0-9_$]*)/);
+ if (stylesVarMatch && variableArrays[stylesVarMatch[1]]) {
+ gallery.styles = variableArrays[stylesVarMatch[1]];
+ }
+
+ // Extract sizeProp
+ const sizePropMatch = galleryContent.match(/sizeProp:\s*['"]([^'"]+)['"]/);
+ if (sizePropMatch) gallery.sizeProp = sizePropMatch[1];
+
+ // Extract styleProp
+ const stylePropMatch = galleryContent.match(/styleProp:\s*['"]([^'"]+)['"]/);
+ if (stylePropMatch) gallery.styleProp = stylePropMatch[1];
+ }
+ }
+
+ // Extract liveExample - template literal for custom live code block
+ const liveExampleMatch = parametersContent.match(/liveExample:\s*`/);
+ if (liveExampleMatch) {
+ // Find the closing backtick
+ const startIndex = liveExampleMatch.index + liveExampleMatch[0].length;
+ let endIndex = startIndex;
+ while (endIndex < parametersContent.length && parametersContent[endIndex] !== '`') {
+ // Handle escaped backticks
+ if (parametersContent[endIndex] === '\\' && parametersContent[endIndex + 1] === '`') {
+ endIndex += 2;
+ } else {
+ endIndex++;
+ }
+ }
+ if (endIndex < parametersContent.length) {
+ // Unescape template literal escapes (source text has \` and \$ for literal backticks/dollars)
+ liveExample = parametersContent.slice(startIndex, endIndex).replace(/\\`/g, '`').replace(/\\\$/g, '$');
+ }
+ }
+
+ // Extract renderComponent - allows overriding which component to render
+ // Useful when the title-derived component (e.g., 'Icons') is a namespace, not a component
+ const renderComponentMatch = parametersContent.match(/renderComponent:\s*['"]([^'"]+)['"]/);
+ if (renderComponentMatch) {
+ renderComponent = renderComponentMatch[1];
+ }
+
+ // Extract triggerProp/onHideProp - for components like Modal that need a trigger button
+ const triggerPropMatch = parametersContent.match(/triggerProp:\s*['"]([^'"]+)['"]/);
+ if (triggerPropMatch) {
+ triggerProp = triggerPropMatch[1];
+ }
+ const onHidePropMatch = parametersContent.match(/onHideProp:\s*['"]([^'"]+)['"]/);
+ if (onHidePropMatch) {
+ onHideProp = onHidePropMatch[1];
+ }
+
+ // Extract examples array - for multiple code examples
+ // Format: examples: [{ title: 'Title', code: `...` }, ...]
+ const examplesMatch = parametersContent.match(/examples:\s*\[/);
+ if (examplesMatch) {
+ const examplesStartIndex = examplesMatch.index + examplesMatch[0].length - 1;
+ const examplesArrayContent = extractBalancedBrackets(parametersContent, examplesStartIndex);
+ if (examplesArrayContent) {
+ examples = [];
+ // Find each example object { title: '...', code: `...` }
+ const exampleObjPattern = /\{\s*title:\s*['"]([^'"]+)['"]\s*,\s*code:\s*`/g;
+ let exampleMatch;
+ while ((exampleMatch = exampleObjPattern.exec(examplesArrayContent)) !== null) {
+ const title = exampleMatch[1];
+ const codeStartIndex = exampleMatch.index + exampleMatch[0].length;
+ // Find closing backtick for code
+ let codeEndIndex = codeStartIndex;
+ while (codeEndIndex < examplesArrayContent.length && examplesArrayContent[codeEndIndex] !== '`') {
+ if (examplesArrayContent[codeEndIndex] === '\\' && examplesArrayContent[codeEndIndex + 1] === '`') {
+ codeEndIndex += 2;
+ } else {
+ codeEndIndex++;
+ }
+ }
+ // Unescape template literal escapes (source text has \` and \$ for literal backticks/dollars)
+ const code = examplesArrayContent.slice(codeStartIndex, codeEndIndex).replace(/\\`/g, '`').replace(/\\\$/g, '$');
+ examples.push({ title, code });
+ }
+ }
+ }
+ }
+ }
+
+ if (sampleChildren || gallery || staticProps || liveExample || examples || renderComponent || triggerProp) break;
+ }
+
+ return { sampleChildren, sampleChildrenStyle, gallery, staticProps, liveExample, examples, renderComponent, triggerProp, onHideProp };
+}
+
+/**
+ * Extract args and controls from story content
+ */
+function extractArgsAndControls(content, componentName) {
+ const args = {};
+ const argTypes = {};
+
+ // First, extract argTypes from the default export meta (shared across all stories)
+ // Pattern: export default { argTypes: {...} }
+ const defaultExportMatch = content.match(/export\s+default\s*\{/);
+ if (defaultExportMatch) {
+ const metaContent = extractBalancedBraces(content, defaultExportMatch.index + defaultExportMatch[0].length - 1);
+ if (metaContent) {
+ const metaArgTypesMatch = metaContent.match(/\bargTypes:\s*\{/);
+ if (metaArgTypesMatch) {
+ const metaArgTypesContent = extractBalancedBraces(metaContent, metaArgTypesMatch.index + metaArgTypesMatch[0].length - 1);
+ if (metaArgTypesContent) {
+ parseArgTypes(metaArgTypesContent, argTypes, content);
+ }
+ }
+ }
+ }
+
+ // Then, try to find the Interactive story block (CSF 3.0 or CSF 2.0)
+ // Support multiple naming conventions:
+ // - InteractiveComponentName (CSF 2.0 convention)
+ // - ComponentNameStory (CSF 3.0 convention)
+ // - ComponentName (fallback)
+ const storyNames = [`Interactive${componentName}`, `${componentName}Story`, componentName];
+
+ // Extract docs config (sampleChildren, sampleChildrenStyle, gallery, staticProps, liveExample) from parameters.docs
+ const { sampleChildren, sampleChildrenStyle, gallery, staticProps, liveExample, examples, renderComponent, triggerProp, onHideProp } = extractDocsConfig(content, storyNames);
+
+ for (const storyName of storyNames) {
+ // Try CSF 3.0 format: export const StoryName: StoryObj = { args: {...}, argTypes: {...} }
+ const csf3Pattern = new RegExp(`export\\s+const\\s+${storyName}[^=]*=[^{]*\\{`, 's');
+ const csf3Match = content.match(csf3Pattern);
+
+ if (csf3Match) {
+ const storyStartIndex = csf3Match.index + csf3Match[0].length - 1;
+ const storyContent = extractBalancedBraces(content, storyStartIndex);
+
+ if (storyContent) {
+ // Extract args from story content
+ const argsMatch = storyContent.match(/\bargs:\s*\{/);
+ if (argsMatch) {
+ const argsContent = extractBalancedBraces(storyContent, argsMatch.index + argsMatch[0].length - 1);
+ if (argsContent) {
+ parseArgsContent(argsContent, args);
+ }
+ }
+
+ // Extract argTypes from story content
+ const argTypesMatch = storyContent.match(/\bargTypes:\s*\{/);
+ if (argTypesMatch) {
+ const argTypesContent = extractBalancedBraces(storyContent, argTypesMatch.index + argTypesMatch[0].length - 1);
+ if (argTypesContent) {
+ parseArgTypes(argTypesContent, argTypes, content);
+ }
+ }
+
+ if (Object.keys(args).length > 0 || Object.keys(argTypes).length > 0) {
+ break; // Found a matching story
+ }
+ }
+ }
+
+ // Try CSF 2.0 format: StoryName.args = {...}
+ const csf2ArgsPattern = new RegExp(`${storyName}\\.args\\s*=\\s*\\{`, 's');
+ const csf2ArgsMatch = content.match(csf2ArgsPattern);
+ if (csf2ArgsMatch) {
+ const argsContent = extractBalancedBraces(content, csf2ArgsMatch.index + csf2ArgsMatch[0].length - 1);
+ if (argsContent) {
+ parseArgsContent(argsContent, args);
+ }
+ }
+
+ // Try CSF 2.0 argTypes: StoryName.argTypes = {...}
+ const csf2ArgTypesPattern = new RegExp(`${storyName}\\.argTypes\\s*=\\s*\\{`, 's');
+ const csf2ArgTypesMatch = content.match(csf2ArgTypesPattern);
+ if (csf2ArgTypesMatch) {
+ const argTypesContent = extractBalancedBraces(content, csf2ArgTypesMatch.index + csf2ArgTypesMatch[0].length - 1);
+ if (argTypesContent) {
+ parseArgTypes(argTypesContent, argTypes, content);
+ }
+ }
+
+ if (Object.keys(args).length > 0 || Object.keys(argTypes).length > 0) {
+ break; // Found a matching story
+ }
+ }
+
+ // Generate controls from args first, then add any argTypes-only props
+ const controls = [];
+ const processedProps = new Set();
+
+ // First pass: props that have default values in args
+ for (const [key, value] of Object.entries(args)) {
+ processedProps.add(key);
+ const label = propNameToLabel(key);
+ const argType = argTypes[key] || {};
+
+ if (argType.type) {
+ // Use argTypes override (select, radio with options)
+ controls.push({
+ name: key,
+ label,
+ type: argType.type,
+ options: argType.options,
+ description: argType.description
+ });
+ } else if (typeof value === 'boolean') {
+ controls.push({ name: key, label, type: 'boolean', description: argType.description });
+ } else if (typeof value === 'string') {
+ controls.push({ name: key, label, type: 'text', description: argType.description });
+ } else if (typeof value === 'number') {
+ controls.push({ name: key, label, type: 'number', description: argType.description });
+ }
+ }
+
+ // Second pass: props defined only in argTypes (no explicit value in args)
+ // Add controls for these, but don't set default values on the component
+ // (setting defaults like open: false or status: 'error' breaks component behavior)
+ for (const [key, argType] of Object.entries(argTypes)) {
+ if (processedProps.has(key)) continue;
+ if (!argType.type) continue; // Skip if no control type defined
+
+ const label = propNameToLabel(key);
+
+ // Don't add to args - let the component use its own defaults
+
+ controls.push({
+ name: key,
+ label,
+ type: argType.type,
+ options: argType.options,
+ description: argType.description
+ });
+ }
+
+ return { args, argTypes, controls, sampleChildren, sampleChildrenStyle, gallery, staticProps, liveExample, examples, renderComponent, triggerProp, onHideProp };
+}
+
+/**
+ * Generate MDX content for a component
+ */
+function generateMDX(component, storyContent) {
+ const { componentName, description, relativePath, category, sourceConfig, resolvedImportPath, isDefaultExport } = component;
+
+ const { args, argTypes, controls, sampleChildren, sampleChildrenStyle, gallery, staticProps, liveExample, examples, renderComponent, triggerProp, onHideProp } = extractArgsAndControls(storyContent, componentName);
+
+ // Merge staticProps into args for complex values (arrays, objects) that can't be parsed from inline args
+ const mergedArgs = { ...args, ...staticProps };
+
+ // Format JSON: unquote property names but keep double quotes for string values
+ // This avoids issues with single quotes in strings breaking MDX parsing
+ const controlsJson = JSON.stringify(controls, null, 2)
+ .replace(/"(\w+)":/g, '$1:');
+
+ const propsJson = JSON.stringify(mergedArgs, null, 2)
+ .replace(/"(\w+)":/g, '$1:');
+
+ // Format sampleChildren if present (from story's parameters.docs.sampleChildren)
+ const sampleChildrenJson = sampleChildren
+ ? JSON.stringify(sampleChildren)
+ : null;
+
+ // Format sampleChildrenStyle if present (from story's parameters.docs.sampleChildrenStyle)
+ const sampleChildrenStyleJson = sampleChildrenStyle
+ ? JSON.stringify(sampleChildrenStyle).replace(/"(\w+)":/g, '$1:')
+ : null;
+
+ // Format gallery config if present
+ const hasGallery = gallery && gallery.sizes && gallery.styles;
+
+ // Extract children for proper JSX rendering
+ const childrenValue = mergedArgs.children;
+
+ const liveExampleProps = Object.entries(mergedArgs)
+ .filter(([key]) => key !== 'children')
+ .map(([key, value]) => {
+ if (typeof value === 'string') return `${key}="${value}"`;
+ if (typeof value === 'boolean') return value ? key : null;
+ return `${key}={${JSON.stringify(value)}}`;
+ })
+ .filter(Boolean)
+ .join('\n ');
+
+ // Generate props table with descriptions from argTypes
+ const propsTable = Object.entries(mergedArgs).map(([key, value]) => {
+ const type = typeof value === 'boolean' ? 'boolean' : typeof value === 'string' ? 'string' : typeof value === 'number' ? 'number' : 'any';
+ const desc = argTypes[key]?.description || '-';
+ return `| \`${key}\` | \`${type}\` | \`${JSON.stringify(value)}\` | ${desc} |`;
+ }).join('\n');
+
+ // Calculate relative import path based on category depth
+ const importDepth = category.includes('/') ? 4 : 3;
+ const wrapperImportPrefix = '../'.repeat(importDepth);
+
+ // Use resolved import path if available, otherwise fall back to source config
+ const componentImportPath = resolvedImportPath || sourceConfig.importPrefix;
+
+ // Determine component description based on source
+ const defaultDesc = sourceConfig.category === 'ui'
+ ? `The ${componentName} component from Superset's UI library.`
+ : `The ${componentName} component from Superset.`;
+
+ return `---
+title: ${componentName}
+sidebar_label: ${componentName}
+---
+
+
+
+import { StoryWithControls${hasGallery ? ', ComponentGallery' : ''} } from '${wrapperImportPrefix}src/components/StorybookWrapper';
+
+# ${componentName}
+
+${description || defaultDesc}
+${hasGallery ? `
+## All Variants
+
+
+` : ''}
+## Live Example
+
+
+
+## Try It
+
+Edit the code below to experiment with the component:
+
+\`\`\`tsx live
+${liveExample || `function Demo() {
+ return (
+ <${componentName}
+ ${liveExampleProps || '// Add props here'}
+ ${childrenValue ? `>
+ ${childrenValue}
+ ${componentName}>` : '/>'}
+ );
+}`}
+\`\`\`
+${examples && examples.length > 0 ? examples.map(ex => `
+## ${ex.title}
+
+\`\`\`tsx live
+${ex.code}
+\`\`\`
+`).join('') : ''}
+${Object.keys(args).length > 0 ? `## Props
+
+| Prop | Type | Default | Description |
+|------|------|---------|-------------|
+${propsTable}` : ''}
+
+## Import
+
+\`\`\`tsx
+${isDefaultExport ? `import ${componentName} from '${componentImportPath}';` : `import { ${componentName} } from '${componentImportPath}';`}
+\`\`\`
+
+---
+
+:::tip[Improve this page]
+This documentation is auto-generated from the component's Storybook story.
+Help improve it by [editing the story file](https://github.com/apache/superset/edit/master/${relativePath}).
+:::
+`;
+}
+
+/**
+ * Category display names for sidebar
+ */
+const CATEGORY_LABELS = {
+ ui: { title: 'Core Components', sidebarLabel: 'Core Components', description: 'Buttons, inputs, modals, selects, and other fundamental UI elements.' },
+ 'design-system': { title: 'Layout Components', sidebarLabel: 'Layout Components', description: 'Grid, Layout, Table, Flex, Space, and container components for page structure.' },
+};
+
+/**
+ * Generate category index page
+ */
+function generateCategoryIndex(category, components) {
+ const labels = CATEGORY_LABELS[category] || {
+ title: category.charAt(0).toUpperCase() + category.slice(1).replace(/-/g, ' '),
+ sidebarLabel: category.charAt(0).toUpperCase() + category.slice(1).replace(/-/g, ' '),
+ };
+ const componentList = components
+ .sort((a, b) => a.componentName.localeCompare(b.componentName))
+ .map(c => `- [${c.componentName}](./${c.componentName.toLowerCase()})`)
+ .join('\n');
+
+ return `---
+title: ${labels.title}
+sidebar_label: ${labels.sidebarLabel}
+sidebar_position: 1
+---
+
+
+
+# ${labels.title}
+
+${components.length} components available in this category.
+
+## Components
+
+${componentList}
+`;
+}
+
+/**
+ * Generate main overview page
+ */
+function generateOverviewIndex(categories) {
+ const categoryList = Object.entries(categories)
+ .filter(([, components]) => components.length > 0)
+ .map(([cat, components]) => {
+ const labels = CATEGORY_LABELS[cat] || {
+ title: cat.charAt(0).toUpperCase() + cat.slice(1).replace(/-/g, ' '),
+ };
+ const desc = labels.description ? ` ${labels.description}` : '';
+ return `### [${labels.title}](./${cat}/)\n${components.length} components —${desc}\n`;
+ })
+ .join('\n');
+
+ const totalComponents = Object.values(categories).reduce((sum, c) => sum + c.length, 0);
+
+ return `---
+title: UI Components Overview
+sidebar_label: Overview
+sidebar_position: 0
+---
+
+
+
+# Superset Design System
+
+A design system is a complete set of standards intended to manage design at scale using reusable components and patterns.
+
+The Superset Design System uses [Atomic Design](https://bradfrost.com/blog/post/atomic-web-design/) principles with adapted terminology:
+
+| Atomic Design | Atoms | Molecules | Organisms | Templates | Pages / Screens |
+|---|:---:|:---:|:---:|:---:|:---:|
+| **Superset Design** | Foundations | Components | Patterns | Templates | Features |
+
+
+
+---
+
+## Component Library
+
+Interactive documentation for Superset's UI component library. **${totalComponents} components** documented across ${Object.keys(categories).filter(k => categories[k].length > 0).length} categories.
+
+${categoryList}
+
+## Usage
+
+All components are exported from \`@superset-ui/core/components\`:
+
+\`\`\`tsx
+import { Button, Modal, Select } from '@superset-ui/core/components';
+\`\`\`
+
+## Contributing
+
+This documentation is auto-generated from Storybook stories. To add or update component documentation:
+
+1. Create or update the component's \`.stories.tsx\` file
+2. Add a descriptive \`title\` and \`description\` in the story meta
+3. Export an interactive story with \`args\` for configurable props
+4. Run \`yarn generate:superset-components\` in the \`docs/\` directory
+
+:::info Work in Progress
+This component library is actively being documented. See the [Components TODO](./TODO) page for a list of components awaiting documentation.
+:::
+
+---
+
+*Auto-generated from Storybook stories in the [Design System/Introduction](https://github.com/apache/superset/blob/master/superset-frontend/packages/superset-ui-core/src/components/DesignSystem.stories.tsx) story.*
+`;
+}
+
+/**
+ * Generate TODO.md tracking skipped components
+ */
+function generateTodoMd(skippedFiles) {
+ const disabledSources = SOURCES.filter(s => !s.enabled);
+ const grouped = {};
+ for (const file of skippedFiles) {
+ const source = disabledSources.find(s => file.includes(s.path));
+ const sourceName = source ? source.name : 'unknown';
+ if (!grouped[sourceName]) grouped[sourceName] = [];
+ grouped[sourceName].push(file);
+ }
+
+ const sections = Object.entries(grouped)
+ .map(([source, files]) => {
+ const fileList = files.map(f => `- [ ] \`${path.relative(ROOT_DIR, f)}\``).join('\n');
+ return `### ${source}\n\n${files.length} components\n\n${fileList}`;
+ })
+ .join('\n\n');
+
+ return `---
+title: Components TODO
+sidebar_class_name: hidden
+---
+
+# Components TODO
+
+These components were found but not yet supported for documentation generation.
+Future phases will add support for these sources.
+
+## Summary
+
+- **Total skipped:** ${skippedFiles.length} story files
+- **Reason:** Import path resolution not yet implemented
+
+## Skipped by Source
+
+${sections}
+
+## How to Add Support
+
+1. Determine the correct import path for the source
+2. Update \`generate-superset-components.mjs\` to handle the source
+3. Add source to \`SUPPORTED_SOURCES\` array
+4. Re-run the generator
+
+---
+
+*Auto-generated by generate-superset-components.mjs*
+`;
+}
+
+/**
+ * Main function
+ */
+async function main() {
+ console.log('Generating Superset Components documentation...\n');
+
+ // Find enabled story files
+ const enabledFiles = findEnabledStoryFiles();
+ console.log(`Found ${enabledFiles.length} story files from enabled sources\n`);
+
+ // Find disabled story files (for tracking)
+ const disabledFiles = findDisabledStoryFiles();
+ console.log(`Found ${disabledFiles.length} story files from disabled sources (tracking only)\n`);
+
+ // Parse enabled files
+ const components = [];
+ for (const { file, source } of enabledFiles) {
+ const parsed = parseStoryFile(file, source);
+ if (parsed && parsed.componentName) {
+ components.push(parsed);
+ }
+ }
+
+ console.log(`Parsed ${components.length} components\n`);
+
+ // Group by category
+ const categories = {};
+ for (const component of components) {
+ if (!categories[component.category]) {
+ categories[component.category] = [];
+ }
+ categories[component.category].push(component);
+ }
+
+ // Ensure output directory exists
+ if (!fs.existsSync(OUTPUT_DIR)) {
+ fs.mkdirSync(OUTPUT_DIR, { recursive: true });
+ }
+
+ // Generate MDX files by category
+ let generatedCount = 0;
+ for (const [category, categoryComponents] of Object.entries(categories)) {
+ const categoryDir = path.join(OUTPUT_DIR, category);
+ if (!fs.existsSync(categoryDir)) {
+ fs.mkdirSync(categoryDir, { recursive: true });
+ }
+
+ // Generate component pages
+ for (const component of categoryComponents) {
+ const storyContent = fs.readFileSync(component.filePath, 'utf-8');
+ const mdxContent = generateMDX(component, storyContent);
+ const outputPath = path.join(categoryDir, `${component.componentName.toLowerCase()}.mdx`);
+ fs.writeFileSync(outputPath, mdxContent);
+ console.log(` ✓ ${category}/${component.componentName}`);
+ generatedCount++;
+ }
+
+ // Generate category index
+ const indexContent = generateCategoryIndex(category, categoryComponents);
+ const indexPath = path.join(categoryDir, 'index.mdx');
+ fs.writeFileSync(indexPath, indexContent);
+ console.log(` ✓ ${category}/index`);
+ }
+
+ // Generate main overview
+ const overviewContent = generateOverviewIndex(categories);
+ const overviewPath = path.join(OUTPUT_DIR, 'index.mdx');
+ fs.writeFileSync(overviewPath, overviewContent);
+ console.log(` ✓ index (overview)`);
+
+ // Generate TODO.md
+ const todoContent = generateTodoMd(disabledFiles);
+ const todoPath = path.join(OUTPUT_DIR, 'TODO.md');
+ fs.writeFileSync(todoPath, todoContent);
+ console.log(` ✓ TODO.md`);
+
+ console.log(`\nDone! Generated ${generatedCount} component pages.`);
+ console.log(`Tracked ${disabledFiles.length} components for future implementation.`);
+}
+
+main().catch(console.error);
diff --git a/docs/sidebarTutorials.js b/docs/sidebarTutorials.js
index 6e0eff12247d..b527517a9540 100644
--- a/docs/sidebarTutorials.js
+++ b/docs/sidebarTutorials.js
@@ -110,9 +110,21 @@ const sidebars = {
'testing/frontend-testing',
'testing/backend-testing',
'testing/e2e-testing',
+ 'testing/storybook',
'testing/ci-cd',
],
},
+ {
+ type: 'category',
+ label: 'UI Components',
+ collapsed: true,
+ items: [
+ {
+ type: 'autogenerated',
+ dirName: 'components',
+ },
+ ],
+ },
{
type: 'link',
label: 'API Reference',
diff --git a/docs/src/components/BlurredSection.tsx b/docs/src/components/BlurredSection.tsx
index d502cadcb3db..712a36fa8c51 100644
--- a/docs/src/components/BlurredSection.tsx
+++ b/docs/src/components/BlurredSection.tsx
@@ -39,11 +39,12 @@ const StyledBlurredSection = styled('section')`
interface BlurredSectionProps {
children: ReactNode;
+ id?: string;
}
-const BlurredSection = ({ children }: BlurredSectionProps) => {
+const BlurredSection = ({ children, id }: BlurredSectionProps) => {
return (
-
+
{children}
diff --git a/docs/src/components/StorybookWrapper.jsx b/docs/src/components/StorybookWrapper.jsx
index ddb1d899ba12..86b1569a872d 100644
--- a/docs/src/components/StorybookWrapper.jsx
+++ b/docs/src/components/StorybookWrapper.jsx
@@ -18,33 +18,245 @@
*/
import React from 'react';
-import { supersetTheme, ThemeProvider } from '@superset-ui/core';
+import BrowserOnly from '@docusaurus/BrowserOnly';
+
+// Lazy-loaded component registry - populated on first use in browser
+let componentRegistry = null;
+let SupersetProviders = null;
+
+function getComponentRegistry() {
+ if (typeof window === 'undefined') {
+ return {}; // SSR - return empty
+ }
+
+ if (componentRegistry !== null) {
+ return componentRegistry; // Already loaded
+ }
+
+ try {
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
+ const antd = require('antd');
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
+ const SupersetComponents = require('@superset/components');
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
+ const CoreUI = require('@apache-superset/core/ui');
+
+ // Build component registry with antd as base fallback layer.
+ // Some Superset components (e.g., Typography) use styled-components that may
+ // fail to initialize in the docs build. Antd originals serve as fallbacks.
+ componentRegistry = { ...antd, ...SupersetComponents, ...CoreUI };
+
+ return componentRegistry;
+ } catch (error) {
+ console.error('[StorybookWrapper] Failed to load components:', error);
+ componentRegistry = {};
+ return componentRegistry;
+ }
+}
+
+function getProviders() {
+ if (typeof window === 'undefined') {
+ return ({ children }) => children; // SSR
+ }
+
+ if (SupersetProviders !== null) {
+ return SupersetProviders;
+ }
+
+ try {
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
+ const { themeObject } = require('@apache-superset/core/ui');
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
+ const { App, ConfigProvider } = require('antd');
+
+ // Configure Ant Design to render portals (tooltips, dropdowns, etc.)
+ // inside the closest .storybook-example container instead of document.body
+ // This fixes positioning issues in the docs pages
+ const getPopupContainer = (triggerNode) => {
+ // Find the closest .storybook-example container
+ const container = triggerNode?.closest?.('.storybook-example');
+ return container || document.body;
+ };
+
+ SupersetProviders = ({ children }) => (
+
+ document.body}
+ >
+ {children}
+
+
+ );
+ return SupersetProviders;
+ } catch (error) {
+ console.error('[StorybookWrapper] Failed to load providers:', error);
+ return ({ children }) => children;
+ }
+}
+
+// Check if a value is a valid React component (function, forwardRef, memo, etc.)
+function isReactComponent(value) {
+ if (!value) return false;
+ // Function/class components
+ if (typeof value === 'function') return true;
+ // forwardRef, memo, lazy — React wraps these as objects with $$typeof
+ if (typeof value === 'object' && value.$$typeof) return true;
+ return false;
+}
+
+// Resolve component from string name or React component
+// Supports dot notation for nested components (e.g., 'Icons.InfoCircleOutlined')
+function resolveComponent(component) {
+ if (!component) return null;
+ // If already a component (function/class/forwardRef), return as-is
+ if (isReactComponent(component)) return component;
+ // If string, look up in registry
+ if (typeof component === 'string') {
+ const registry = getComponentRegistry();
+ // Handle dot notation (e.g., 'Icons.InfoCircleOutlined')
+ if (component.includes('.')) {
+ const parts = component.split('.');
+ let current = registry[parts[0]];
+ for (let i = 1; i < parts.length && current; i++) {
+ current = current[parts[i]];
+ }
+ return isReactComponent(current) ? current : null;
+ }
+ return registry[component] || null;
+ }
+ return null;
+}
+
+// Loading placeholder for SSR
+function LoadingPlaceholder() {
+ return (
+
+ Loading component...
+
+ );
+}
// A simple component to display a story example
-export function StoryExample({ component: Component, props = {} }) {
+export function StoryExample({ component, props = {} }) {
return (
-
-