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} + ` : '/>'} + ); +}`} +\`\`\` +${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 | + +Atoms = Foundations, Molecules = Components, Organisms = Patterns, Templates = Templates, Pages / Screens = 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} Blur 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 ( - -
- {Component && } -
-
+ }> + {() => { + const Component = resolveComponent(component); + const Providers = getProviders(); + const { children, restProps } = extractChildren(props); + return ( + +
+ {Component ? ( + {children} + ) : ( +
+ Component "{String(component)}" not found +
+ )} +
+
+ ); + }} +
); } -// A simple component to display a story with controls -export function StoryWithControls({ - component: Component, - props = {}, - controls = [], -}) { +// Props that should be rendered as children rather than passed as props +const CHILDREN_PROP_NAMES = ['label', 'children', 'text', 'content']; + +// Extract children from props based on common conventions +function extractChildren(props) { + for (const propName of CHILDREN_PROP_NAMES) { + if (props[propName] !== undefined && props[propName] !== null && props[propName] !== '') { + const { [propName]: childContent, ...restProps } = props; + return { children: childContent, restProps }; + } + } + return { children: null, restProps: props }; +} + +// Generate sample children for layout components +// Supports: +// - Array of strings: ['Item 1', 'Item 2'] - renders as styled divs +// - Array of component descriptors: [{ component: 'Button', props: { children: 'Click' } }] +// - Number: 3 - generates that many sample items +// - String: 'content' - renders as literal content +function generateSampleChildren(sampleChildren, sampleChildrenStyle) { + if (!sampleChildren) return null; + + // Default style if none provided (minimal, just enough to see items) + const itemStyle = sampleChildrenStyle || {}; + + // If it's an array, check if items are component descriptors or strings + if (Array.isArray(sampleChildren)) { + return sampleChildren.map((item, i) => { + // Component descriptor: { component: 'Button', props: { ... } } + if (item && typeof item === 'object' && item.component) { + const ChildComponent = resolveComponent(item.component); + if (ChildComponent) { + return ; + } + // Fallback if component not found + return
{item.props?.children || `Unknown: ${item.component}`}
; + } + // Simple string + return ( +
+ {item} +
+ ); + }); + } + + // If it's a number, generate that many sample items + if (typeof sampleChildren === 'number') { + return new Array(sampleChildren).fill(null).map((_, i) => ( +
+ Item {i + 1} +
+ )); + } + + // If it's a string, treat as literal content + if (typeof sampleChildren === 'string') { + return sampleChildren; + } + + return sampleChildren; +} + +// Inner component for StoryWithControls (browser-only) +// renderComponent allows overriding which component to actually render (useful when the named +// component is a namespace object like Icons, not a React component) +// triggerProp: for components like Modal that need a trigger, specify the boolean prop that controls visibility +function StoryWithControlsInner({ component, renderComponent, props, controls, sampleChildren, sampleChildrenStyle, triggerProp, onHideProp }) { + // Use renderComponent if provided, otherwise use the main component name + const componentToRender = renderComponent || component; + const Component = resolveComponent(componentToRender); + const Providers = getProviders(); const [stateProps, setStateProps] = React.useState(props); const updateProp = (key, value) => { @@ -54,8 +266,77 @@ export function StoryWithControls({ })); }; + // Extract children from props (label, children, text, content) + // When sampleChildren is explicitly provided, skip extraction so all props + // (like 'content') stay as component props rather than becoming children + const { children: propsChildren, restProps } = sampleChildren + ? { children: null, restProps: stateProps } + : extractChildren(stateProps); + // Filter out undefined values so they don't override component defaults + const filteredProps = Object.fromEntries( + Object.entries(restProps).filter(([, v]) => v !== undefined) + ); + + // Resolve any prop values that are component descriptors + // e.g., { component: 'Button', props: { children: 'Click' } } + // Also resolves descriptors nested inside array items: + // e.g., items: [{ id: 'x', element: { component: 'div', props: { children: 'text' } } }] + Object.keys(filteredProps).forEach(key => { + const value = filteredProps[key]; + if (value && typeof value === 'object' && !Array.isArray(value) && value.component) { + const PropComponent = resolveComponent(value.component); + if (PropComponent) { + filteredProps[key] = ; + } + } + if (Array.isArray(value)) { + filteredProps[key] = value.map((item, idx) => { + if (item && typeof item === 'object') { + const resolved = { ...item }; + Object.keys(resolved).forEach(field => { + const fieldValue = resolved[field]; + if (fieldValue && typeof fieldValue === 'object' && !Array.isArray(fieldValue) && fieldValue.component) { + const FieldComponent = resolveComponent(fieldValue.component); + if (FieldComponent) { + resolved[field] = React.createElement(FieldComponent, { key: `${key}-${idx}`, ...fieldValue.props }); + } + } + }); + return resolved; + } + return item; + }); + } + }); + + // For List-like components with dataSource but no renderItem, provide a default + if (filteredProps.dataSource && !filteredProps.renderItem) { + const ListItem = resolveComponent('List')?.Item; + filteredProps.renderItem = (item) => + ListItem + ? React.createElement(ListItem, null, String(item)) + : React.createElement('div', null, String(item)); + } + + // Use sample children if provided, otherwise use props children + const children = generateSampleChildren(sampleChildren, sampleChildrenStyle) || propsChildren; + + // For components with a trigger (like Modal with show/onHide), add handlers. + // onHideProp supports comma-separated names for components with multiple close + // callbacks (e.g., "onHide,handleSave,onConfirmNavigation"). + const triggerProps = {}; + if (triggerProp && onHideProp) { + const closeHandler = () => updateProp(triggerProp, false); + onHideProp.split(',').forEach(prop => { + triggerProps[prop.trim()] = closeHandler; + }); + } + + // Get the Button component for trigger buttons + const ButtonComponent = resolveComponent('Button'); + return ( - +
- {Component && } + {Component ? ( + <> + {/* Show a trigger button for components like Modal */} + {triggerProp && ButtonComponent && ( + updateProp(triggerProp, true)}> + Open {component} + + )} + {children} + + ) : ( +
+ Component "{String(componentToRender)}" not found +
+ )}
{controls.length > 0 && ( @@ -87,26 +383,64 @@ export function StoryWithControls({ {control.type === 'select' ? ( + ) : control.type === 'inline-radio' || control.type === 'radio' ? ( +
+ {control.options?.map(option => ( + + ))} +
) : control.type === 'boolean' ? ( updateProp(control.name, e.target.checked)} /> + ) : control.type === 'number' ? ( + updateProp(control.name, Number(e.target.value))} + style={{ width: '100%', padding: '5px' }} + /> + ) : control.type === 'color' ? ( + updateProp(control.name, e.target.value)} + style={{ + width: '50px', + height: '30px', + padding: '2px', + cursor: 'pointer', + }} + /> ) : ( updateProp(control.name, e.target.value)} style={{ width: '100%', padding: '5px' }} /> @@ -116,6 +450,81 @@ export function StoryWithControls({
)} -
+ + ); +} + +// A simple component to display a story with controls +// renderComponent: optional override for which component to render (e.g., 'Icons.InfoCircleOutlined' when component='Icons') +// triggerProp/onHideProp: for components like Modal that need a button to open (e.g., triggerProp="show", onHideProp="onHide") +export function StoryWithControls({ component: Component, renderComponent, props = {}, controls = [], sampleChildren, sampleChildrenStyle, triggerProp, onHideProp }) { + return ( + }> + {() => ( + + )} + + ); +} + +// Inner component for ComponentGallery (browser-only) +function ComponentGalleryInner({ component, sizes, styles, sizeProp, styleProp }) { + const Component = resolveComponent(component); + const Providers = getProviders(); + + if (!Component) { + return ( +
+ Component "{String(component)}" not found +
+ ); + } + + return ( + +
+ {sizes.map(size => ( +
+

{size}

+
+ {styles.map(style => ( + + {style} + + ))} +
+
+ ))} +
+
+ ); +} + +// A component to display a gallery of all variants (sizes x styles) +export function ComponentGallery({ component, sizes = [], styles = [], sizeProp = 'size', styleProp = 'variant' }) { + return ( + }> + {() => ( + + )} + ); } diff --git a/docs/src/components/databases/DatabasePage.tsx b/docs/src/components/databases/DatabasePage.tsx index c02d4a44a1ca..bcd2e1ad1d3b 100644 --- a/docs/src/components/databases/DatabasePage.tsx +++ b/docs/src/components/databases/DatabasePage.tsx @@ -60,8 +60,6 @@ const CodeBlock: React.FC<{ children: React.ReactNode }> = ({ children }) => ( ); const { Title, Paragraph, Text } = Typography; -const { Panel } = Collapse; -const { TabPane } = Tabs; interface DatabasePageProps { database: DatabaseInfo; @@ -112,21 +110,20 @@ const DatabasePage: React.FC = ({ database, name }) => { return ( - - {docs.drivers.map((driver, idx) => ( - - {driver.name} - {driver.is_recommended && ( - - Recommended - - )} - - } - key={idx} - > + ({ + key: String(idx), + label: ( + + {driver.name} + {driver.is_recommended && ( + + Recommended + + )} + + ), + children: ( {driver.pypi_package && (
@@ -145,9 +142,9 @@ const DatabasePage: React.FC = ({ database, name }) => { )} - - ))} - + ), + }))} + /> ); }; @@ -165,46 +162,51 @@ const DatabasePage: React.FC = ({ database, name }) => { } style={{ marginBottom: 16 }} > - - {docs.authentication_methods.map((auth, idx) => ( - - {auth.description && {auth.description}} - {auth.requirements && ( - - )} - {auth.connection_string && - renderConnectionString( - auth.connection_string, - 'Connection String' + ({ + key: String(idx), + label: auth.name, + children: ( + <> + {auth.description && {auth.description}} + {auth.requirements && ( + )} - {auth.secure_extra && ( -
- Secure Extra Configuration: - - {JSON.stringify(auth.secure_extra, null, 2)} - -
- )} - {auth.engine_parameters && ( -
- Engine Parameters: - - {JSON.stringify(auth.engine_parameters, null, 2)} - -
- )} - {auth.notes && ( - - )} -
- ))} -
+ {auth.connection_string && + renderConnectionString( + auth.connection_string, + 'Connection String', + )} + {auth.secure_extra && ( +
+ Secure Extra Configuration: + + {JSON.stringify(auth.secure_extra, null, 2)} + +
+ )} + {auth.engine_parameters && ( +
+ Engine Parameters: + + {JSON.stringify(auth.engine_parameters, null, 2)} + +
+ )} + {auth.notes && ( + + )} + + ), + }))} + /> ); }; @@ -222,23 +224,27 @@ const DatabasePage: React.FC = ({ database, name }) => { } style={{ marginBottom: 16 }} > - - {docs.engine_parameters.map((param, idx) => ( - - {param.description && {param.description}} - {param.json && ( - - {JSON.stringify(param.json, null, 2)} - - )} - {param.docs_url && ( - - Learn more - - )} - - ))} - + ({ + key: String(idx), + label: param.name, + children: ( + <> + {param.description && ( + {param.description} + )} + {param.json && ( + {JSON.stringify(param.json, null, 2)} + )} + {param.docs_url && ( + + Learn more + + )} + + ), + }))} + /> ); }; @@ -247,75 +253,81 @@ const DatabasePage: React.FC = ({ database, name }) => { const renderCompatibleDatabases = () => { if (!docs?.compatible_databases?.length) return null; - // Create array of all panel keys to expand by default - const allPanelKeys = docs.compatible_databases.map((_, idx) => idx); + // Create array of all item keys to expand by default + const allItemKeys = docs.compatible_databases.map((_, idx) => String(idx)); return ( The following databases are compatible with the {name} driver: - - {docs.compatible_databases.map((compat, idx) => ( - - {compat.logo && ( - {compat.name} ({ + key: String(idx), + label: ( +
+ {compat.logo && ( + {compat.name} + )} + {compat.name} +
+ ), + children: ( + <> + {compat.description && ( + {compat.description} + )} + {compat.connection_string && + renderConnectionString(compat.connection_string)} + {compat.parameters && ( +
+ Parameters: + ({ + key, + parameter: key, + description: value, + }), + )} + columns={[ + { + title: 'Parameter', + dataIndex: 'parameter', + key: 'p', + }, + { + title: 'Description', + dataIndex: 'description', + key: 'd', + }, + ]} + pagination={false} + size="small" /> - )} - {compat.name} - - } - key={idx} - > - {compat.description && ( - {compat.description} - )} - {compat.connection_string && - renderConnectionString(compat.connection_string)} - {compat.parameters && ( -
- Parameters: -
({ - key, - parameter: key, - description: value, - }) - )} - columns={[ - { title: 'Parameter', dataIndex: 'parameter', key: 'p' }, - { - title: 'Description', - dataIndex: 'description', - key: 'd', - }, - ]} - pagination={false} - size="small" + + )} + {compat.notes && ( + - - )} - {compat.notes && ( - - )} - - ))} - + )} + + ), + }))} + /> ); }; @@ -376,7 +388,7 @@ const DatabasePage: React.FC = ({ database, name }) => { 'YEAR', ]; const extendedGrains = Object.keys(database.time_grains).filter( - (g) => !commonGrains.includes(g) + g => !commonGrains.includes(g), ); return ( @@ -384,12 +396,14 @@ const DatabasePage: React.FC = ({ database, name }) => {
Common Time Grains:
- {commonGrains.map((grain) => ( + {commonGrains.map(grain => ( ))} @@ -399,12 +413,14 @@ const DatabasePage: React.FC = ({ database, name }) => {
Extended Time Grains:
- {extendedGrains.map((grain) => ( + {extendedGrains.map(grain => ( ))} @@ -471,81 +487,83 @@ const DatabasePage: React.FC = ({ database, name }) => { Common error messages you may encounter when connecting to or querying{' '} {name}, along with their causes and solutions. - - {sortedCategories.map((category) => ( - - - {category} - - {errorsByCategory[category].length} error - {errorsByCategory[category].length !== 1 ? 's' : ''} - - } - key={category} - > - {errorsByCategory[category].map((error, idx) => ( -
-
- {error.description || error.error_type} -
- - {error.invalid_fields && error.invalid_fields.length > 0 && ( + ({ + key: category, + label: ( + + + {category} + + {errorsByCategory[category].length} error + {errorsByCategory[category].length !== 1 ? 's' : ''} + + ), + children: ( + <> + {errorsByCategory[category].map((error, idx) => ( +
- Check these fields: - {error.invalid_fields.map((field) => ( - - {field} - - ))} -
- )} - {error.issue_codes && error.issue_codes.length > 0 && ( -
- Related issue codes: - {error.issue_codes.map((code) => ( - - - Issue {code} - - - ))} + + {error.description || error.error_type} +
- )} -
- ))} - - ))} -
+ + {error.invalid_fields && + error.invalid_fields.length > 0 && ( +
+ Check these fields: + {error.invalid_fields.map(field => ( + + {field} + + ))} +
+ )} + {error.issue_codes && error.issue_codes.length > 0 && ( +
+ Related issue codes: + {error.issue_codes.map(code => ( + + + Issue {code} + + + ))} +
+ )} +
+ ))} + + ), + }))} + /> ); }; return ( -
+
{docs?.logo && ( = ({ database, name }) => { }} /> )} - {name} + + {name} + {docs?.homepage_url && ( = ({ database, name }) => { {docs.pypi_packages?.length > 0 && (
Required packages: - {docs.pypi_packages.map((pkg) => ( + {docs.pypi_packages.map(pkg => ( {pkg} @@ -638,7 +658,7 @@ const DatabasePage: React.FC = ({ database, name }) => { key, parameter: key, description: value, - }) + }), )} columns={[ { title: 'Parameter', dataIndex: 'parameter', key: 'p' }, @@ -664,7 +684,7 @@ const DatabasePage: React.FC = ({ database, name }) => {
{renderConnectionString( example.connection_string, - example.description + example.description, )}
))} @@ -717,7 +737,11 @@ const DatabasePage: React.FC = ({ database, name }) => {
)} {docs.sqlalchemy_docs_url && ( - + SQLAlchemy Dialect Documentation )} diff --git a/docs/src/pages/community.tsx b/docs/src/pages/community.tsx index 09c7621c6982..109720755983 100644 --- a/docs/src/pages/community.tsx +++ b/docs/src/pages/community.tsx @@ -18,7 +18,6 @@ */ import { useState } from 'react'; import styled from '@emotion/styled'; -import { List } from 'antd'; import Layout from '@theme/Layout'; import { mq } from '../utils'; import SectionHeader from '../components/SectionHeader'; @@ -92,8 +91,12 @@ const StyledJoinCommunity = styled('section')` max-width: 540px; margin: 0 auto; padding: 40px 20px 20px 35px; + list-style: none; } .item { + display: flex; + align-items: flex-start; + gap: 12px; padding: 0; border: 0; } @@ -189,39 +192,33 @@ const Community = () => { /> - ( - - - - - } - title={ +
    + {communityLinks.map( + ({ url, title, description, image, ariaLabel }) => ( +
  • + + + +

    {title}

    - } - description={

    {description}

    } - aria-label="Community link" - /> - +

    {description}

    +
    +
  • + ), )} - /> +
- + null; + +// For hooks that return objects/arrays +const useNoop = () => ({}); + +// Mock for useResizeDetector - returns { ref, width, height } where ref.current exists +const useResizeDetectorMock = () => ({ + ref: { current: null }, + width: 0, + height: 0, +}); + +/** + * Creates a recursive proxy that handles any depth of property access. + * This allows patterns like ace.config.set() or ace.config.setModuleUrl() to work. + * + * The proxy is both callable (returns undefined) and accessible (returns another proxy). + */ +function createDeepProxy() { + const handler = { + // Handle property access - return another proxy for chaining + get(target, prop) { + // Standard module properties + if (prop === 'default') return createDeepProxy(); + if (prop === '__esModule') return true; + + // Symbol properties (used by JS internals) + if (typeof prop === 'symbol') { + if (prop === Symbol.toPrimitive) return () => ''; + if (prop === Symbol.toStringTag) return 'NullModule'; + if (prop === Symbol.iterator) return undefined; + return undefined; + } + + // React-specific properties + if (prop === '$$typeof') return undefined; + if (prop === 'propTypes') return undefined; + if (prop === 'displayName') return 'NullComponent'; + + // Specific hook mocks for known hooks that need proper return values + if (prop === 'useResizeDetector') { + return useResizeDetectorMock; + } + + // Common hook names return useNoop for better compatibility + if (typeof prop === 'string' && prop.startsWith('use')) { + return useNoop; + } + + // Return another proxy to allow further chaining (ace.config.set) + return createDeepProxy(); + }, + + // Handle function calls - return undefined (safe default) + apply() { + return undefined; + }, + + // Handle new ClassName() - return an empty object + construct() { + return {}; + }, + }; + + // Create a proxy over a function so it's both callable and has properties + return new Proxy(function NullModule() {}, handler); +} + +// Create the main module export as a deep proxy +const nullModule = createDeepProxy(); + +// Support both CommonJS and ES module patterns +module.exports = nullModule; +module.exports.default = createDeepProxy(); +module.exports.__esModule = true; + +// Named exports for common patterns (webpack may inline these) +module.exports.useResizeDetector = useResizeDetectorMock; +module.exports.withResizeDetector = createDeepProxy(); +module.exports.Resizable = NullComponent; +module.exports.ResizableBox = NullComponent; +module.exports.FixedSizeList = NullComponent; +module.exports.VariableSizeList = NullComponent; + +// ace-builds specific exports that CodeEditor uses +module.exports.config = createDeepProxy(); +module.exports.require = createDeepProxy(); +module.exports.edit = createDeepProxy(); diff --git a/docs/src/shims/react-table.js b/docs/src/shims/react-table.js new file mode 100644 index 000000000000..6c19b1537b3f --- /dev/null +++ b/docs/src/shims/react-table.js @@ -0,0 +1,54 @@ +/** + * 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. + */ + +// Shim for react-table to handle CommonJS to ES module interop +// react-table v7 is CommonJS, but Superset components import it with ES module syntax +// Use relative path to avoid circular dependency since webpack aliases 'react-table' to this file +// eslint-disable-next-line @typescript-eslint/no-require-imports -- CJS interop shim for react-table v7 +const reactTable = require('../../node_modules/react-table'); + +// Re-export all named exports +export const { + useTable, + useFilters, + useSortBy, + usePagination, + useGlobalFilter, + useRowSelect, + useRowState, + useColumnOrder, + useExpanded, + useGroupBy, + useResizeColumns, + useBlockLayout, + useAbsoluteLayout, + useFlexLayout, + actions, + defaultColumn, + makePropGetter, + reduceHooks, + loopHooks, + ensurePluginOrder, + functionalUpdate, + useGetLatest, + safeUseLayoutEffect, +} = reactTable; + +// Default export +export default reactTable; diff --git a/docs/src/styles/custom.css b/docs/src/styles/custom.css index df73738ffc70..bdf74091ff16 100644 --- a/docs/src/styles/custom.css +++ b/docs/src/styles/custom.css @@ -264,3 +264,193 @@ ul.dropdown__menu svg { .menu__list-item.delete.api-method > .menu__link::before { background-color: #f93e3e; } + +/* ============================================ + Component Example Isolation + Prevents Docusaurus/Infima styles from bleeding into Superset components + ============================================ */ + +/* Reset link styles inside component examples */ +.storybook-example a { + color: inherit; + text-decoration: none; + font-weight: inherit; + line-height: inherit; + vertical-align: inherit; +} + +.storybook-example a:hover { + color: inherit; + text-decoration: none; +} + +/* Reset list styles */ +.storybook-example ul, +.storybook-example ol { + margin: 0; + padding: 0; + list-style: none; +} + +/* Override Infima's .markdown li + li margin */ +.storybook-example li + li, +.markdown .storybook-example li + li { + margin-top: 0; +} + +/* Reset heading styles */ +.storybook-example h1, +.storybook-example h2, +.storybook-example h3, +.storybook-example h4, +.storybook-example h5, +.storybook-example h6 { + margin: 0; + font-size: inherit; + font-weight: inherit; +} + +/* Reset paragraph margins */ +.storybook-example p { + margin: 0; +} + +/* Reset table margins - Infima applies margin-bottom via --ifm-spacing-vertical */ +.storybook-example table { + margin: 0; + display: table; +} + +/* Ensure Ant Design components render correctly */ +.storybook-example .ant-breadcrumb { + line-height: 1.5715; +} + +.storybook-example .ant-breadcrumb a { + color: rgba(0, 0, 0, 0.45); +} + +.storybook-example .ant-breadcrumb a:hover { + color: rgba(0, 0, 0, 0.85); +} + +/* ============================================ + Ant Design Popup/Portal Isolation + These components render outside .storybook-example via portals + ============================================ */ + +/* DatePicker, TimePicker dropdown panels - reset Infima table styles + Using doubled selectors for higher specificity than Infima's defaults */ +.ant-picker-dropdown.ant-picker-dropdown table, +.ant-picker-dropdown.ant-picker-dropdown thead, +.ant-picker-dropdown.ant-picker-dropdown tbody, +.ant-picker-dropdown.ant-picker-dropdown tr, +.ant-picker-dropdown.ant-picker-dropdown th, +.ant-picker-dropdown.ant-picker-dropdown td { + border: none; + background: none; + background-color: transparent; +} + +.ant-picker-dropdown.ant-picker-dropdown table { + border-collapse: separate; + border-spacing: 0; + width: 100%; + display: table; +} + +/* Override Infima's zebra striping with higher specificity */ +.ant-picker-dropdown.ant-picker-dropdown tr:nth-child(2n), +.ant-picker-dropdown.ant-picker-dropdown tbody tr:nth-child(2n) { + background: none; + background-color: transparent; +} + +.ant-picker-dropdown.ant-picker-dropdown th, +.ant-picker-dropdown.ant-picker-dropdown td { + padding: 0; +} + +/* Select, Dropdown, Popover portals */ +.ant-select-dropdown.ant-select-dropdown table, +.ant-select-dropdown.ant-select-dropdown thead, +.ant-select-dropdown.ant-select-dropdown tbody, +.ant-select-dropdown.ant-select-dropdown tr, +.ant-select-dropdown.ant-select-dropdown th, +.ant-select-dropdown.ant-select-dropdown td, +.ant-dropdown.ant-dropdown table, +.ant-dropdown.ant-dropdown thead, +.ant-dropdown.ant-dropdown tbody, +.ant-dropdown.ant-dropdown tr, +.ant-dropdown.ant-dropdown th, +.ant-dropdown.ant-dropdown td, +.ant-popover.ant-popover table, +.ant-popover.ant-popover thead, +.ant-popover.ant-popover tbody, +.ant-popover.ant-popover tr, +.ant-popover.ant-popover th, +.ant-popover.ant-popover td { + border: none; + background: none; + background-color: transparent; +} + +.ant-select-dropdown.ant-select-dropdown tr:nth-child(2n), +.ant-dropdown.ant-dropdown tr:nth-child(2n), +.ant-popover.ant-popover tr:nth-child(2n) { + background: none; + background-color: transparent; +} + +/* Modal portals */ +.ant-modal.ant-modal table, +.ant-modal.ant-modal thead, +.ant-modal.ant-modal tbody, +.ant-modal.ant-modal tr, +.ant-modal.ant-modal th, +.ant-modal.ant-modal td { + border: none; + background: none; + background-color: transparent; +} + +.ant-modal.ant-modal tr:nth-child(2n) { + background: none; + background-color: transparent; +} + +/* ============================================ + Live Code Editor Height Limits + Prevents tall code blocks from dominating the page + ============================================ */ + +/* Limit the code editor height and make it scrollable */ +/* Target multiple possible class names used by Docusaurus/react-live */ +.playgroundEditor, +[class*="playgroundEditor"], +.live-editor, +[class*="liveEditor"] { + max-height: 350px !important; + overflow: auto !important; +} + +/* The actual textarea/code area inside the editor */ +.playgroundEditor textarea, +.playgroundEditor pre, +[class*="playgroundEditor"] textarea, +[class*="playgroundEditor"] pre { + max-height: 350px !important; + overflow: auto !important; +} + +/* Also limit the preview area for consistency */ +.playgroundPreview, +[class*="playgroundPreview"] { + max-height: 400px; + overflow: auto; +} + +/* Hide sidebar items with sidebar_class_name: hidden in frontmatter */ +.menu__list-item.hidden { + display: none; +} diff --git a/docs/src/theme.d.ts b/docs/src/theme.d.ts index 04683c86865d..4619708d06d1 100644 --- a/docs/src/theme.d.ts +++ b/docs/src/theme.d.ts @@ -30,3 +30,13 @@ declare module '@theme/Layout' { export default function Layout(props: Props): ReactNode; } + +declare module '@theme/Playground/Header' { + import type { ReactNode } from 'react'; + + export interface Props { + readonly children?: ReactNode; + } + + export default function PlaygroundHeader(props: Props): ReactNode; +} diff --git a/docs/src/theme/Playground/Preview/index.tsx b/docs/src/theme/Playground/Preview/index.tsx new file mode 100644 index 000000000000..42552c185b32 --- /dev/null +++ b/docs/src/theme/Playground/Preview/index.tsx @@ -0,0 +1,107 @@ +/** + * 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. + */ + +import React, { type ReactNode } from 'react'; +import { LiveError, LivePreview } from 'react-live'; +import BrowserOnly from '@docusaurus/BrowserOnly'; +import { ErrorBoundaryErrorMessageFallback } from '@docusaurus/theme-common'; +import ErrorBoundary from '@docusaurus/ErrorBoundary'; +import Translate from '@docusaurus/Translate'; +import PlaygroundHeader from '@theme/Playground/Header'; + +import styles from './styles.module.css'; + +// Get the theme wrapper for Superset components +function getThemeWrapper() { + if (typeof window === 'undefined') { + return ({ children }: { children: React.ReactNode }) => <>{children}; + } + + 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 } = require('antd'); + + if (!themeObject?.SupersetThemeProvider) { + return ({ children }: { children: React.ReactNode }) => <>{children}; + } + + return ({ children }: { children: React.ReactNode }) => ( + + {children} + + ); + } catch (e) { + console.error('[PlaygroundPreview] Failed to load theme provider:', e); + return ({ children }: { children: React.ReactNode }) => <>{children}; + } +} + +function Loader() { + return
Loading...
; +} + +function ThemedLivePreview(): ReactNode { + const ThemeWrapper = getThemeWrapper(); + return ( + + + + ); +} + +function PlaygroundLivePreview(): ReactNode { + // No SSR for the live preview + // See https://github.com/facebook/docusaurus/issues/5747 + return ( + }> + {() => ( + <> + ( + + )} + > + + + + + )} + + ); +} + +export default function PlaygroundPreview(): ReactNode { + return ( + <> + + + Result + + +
+ +
+ + ); +} diff --git a/docs/src/theme/Playground/Preview/styles.module.css b/docs/src/theme/Playground/Preview/styles.module.css new file mode 100644 index 000000000000..178361045f70 --- /dev/null +++ b/docs/src/theme/Playground/Preview/styles.module.css @@ -0,0 +1,23 @@ +/** + * 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. + */ + +.playgroundPreview { + padding: 1rem; + background-color: var(--ifm-pre-background); +} diff --git a/docs/src/theme/ReactLiveScope/index.tsx b/docs/src/theme/ReactLiveScope/index.tsx index 9675a65dd486..27d35cd60520 100644 --- a/docs/src/theme/ReactLiveScope/index.tsx +++ b/docs/src/theme/ReactLiveScope/index.tsx @@ -18,36 +18,49 @@ */ import React from 'react'; -import { Button, Card, Input, Space, Tag, Tooltip } from 'antd'; -// Import extension components from @apache-superset/core/ui -// This matches the established pattern used throughout the Superset codebase -// Resolved via webpack alias to superset-frontend/packages/superset-core/src/ui/components -import { Alert } from '@apache-superset/core/ui'; +// Browser-only check for SSR safety +const isBrowser = typeof window !== 'undefined'; /** * ReactLiveScope provides the scope for live code blocks. * Any component added here will be available in ```tsx live blocks. * - * To add more components: - * 1. Import the component from @apache-superset/core above - * 2. Add it to the scope object below + * Components are conditionally loaded only in the browser to avoid + * SSG issues with Emotion CSS-in-JS jsx runtime. + * + * Components are available by name, e.g.: + * + * + * */ -const ReactLiveScope = { + +// Base scope with React (always available) +const ReactLiveScope: Record = { // React core React, ...React, +}; - // Extension components from @apache-superset/core - Alert, +// Only load Superset components in browser context +// This prevents SSG errors from Emotion CSS-in-JS +if (isBrowser) { + try { + // Dynamic require for browser-only execution + // eslint-disable-next-line @typescript-eslint/no-require-imports + const SupersetComponents = require('@superset/components'); + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { Alert } = require('@apache-superset/core/ui'); - // Common Ant Design components (for demos) - Button, - Card, - Input, - Space, - Tag, - Tooltip, -}; + console.log('[ReactLiveScope] SupersetComponents keys:', Object.keys(SupersetComponents || {}).slice(0, 10)); + console.log('[ReactLiveScope] Has Button?', 'Button' in (SupersetComponents || {})); + + Object.assign(ReactLiveScope, SupersetComponents, { Alert }); + + console.log('[ReactLiveScope] Final scope keys:', Object.keys(ReactLiveScope).slice(0, 20)); + } catch (e) { + console.error('[ReactLiveScope] Failed to load Superset components:', e); + } +} export default ReactLiveScope; diff --git a/docs/src/theme/Root.js b/docs/src/theme/Root.js index 210d99e8ed8b..10538df69704 100644 --- a/docs/src/theme/Root.js +++ b/docs/src/theme/Root.js @@ -74,6 +74,14 @@ export default function Root({ children }) { window._paq.push(['trackSiteSearch', keyword, category, resultsCount]); }; + // Helper to track page views + const trackPageView = (url, title) => { + if (devMode) { + console.log('Matomo trackPageView:', { url, title }); + } + window._paq.push(['trackPageView']); + }; + // Track external link clicks using domain as category (vendor-agnostic) const handleLinkClick = (event) => { @@ -221,7 +229,6 @@ export default function Root({ children }) { trackDocsVersion(); if (devMode) { - console.log('Tracking page view:', currentPath, currentTitle); window._paq.push(['setDomains', ['superset.apache.org']]); window._paq.push([ 'setCustomUrl', @@ -233,7 +240,7 @@ export default function Root({ children }) { window._paq.push(['setReferrerUrl', window.location.href]); window._paq.push(['setDocumentTitle', currentTitle]); - window._paq.push(['trackPageView']); + trackPageView(currentPath, currentTitle); // Check for 404 after page renders setTimeout(track404, 500); diff --git a/docs/src/webpack.extend.ts b/docs/src/webpack.extend.ts index 5d8e0556f2ce..87bc854d2d4c 100644 --- a/docs/src/webpack.extend.ts +++ b/docs/src/webpack.extend.ts @@ -18,6 +18,7 @@ */ import path from 'path'; +import webpack from 'webpack'; import type { Plugin } from '@docusaurus/types'; export default function webpackExtendPlugin(): Plugin { @@ -26,12 +27,73 @@ export default function webpackExtendPlugin(): Plugin { configureWebpack(config) { const isDev = process.env.NODE_ENV === 'development'; + // Use NormalModuleReplacementPlugin to forcefully replace react-table + // This is necessary because regular aliases don't work for modules in nested node_modules + const reactTableShim = path.resolve(__dirname, './shims/react-table.js'); + config.plugins?.push( + new webpack.NormalModuleReplacementPlugin( + /^react-table$/, + reactTableShim, + ), + ); + + // Stub out heavy third-party packages that are transitive dependencies of + // superset-frontend components. The barrel file (components/index.ts) + // re-exports all components, so webpack must resolve their imports even + // though these components are never rendered on the docs site. + const nullModuleShim = path.resolve(__dirname, './shims/null-module.js'); + const heavyDepsPatterns = [ + /^brace(\/|$)/, // ACE editor modes/themes + /^react-ace(\/|$)/, + /^ace-builds(\/|$)/, + /^react-js-cron(\/|$)/, // Cron picker + CSS + // react-resize-detector: NOT shimmed — DropdownContainer needs it at runtime + // for overflow detection. Resolves from superset-frontend/node_modules. + /^react-window(\/|$)/, + /^re-resizable(\/|$)/, + /^react-draggable(\/|$)/, + /^ag-grid-react(\/|$)/, + /^ag-grid-community(\/|$)/, + ]; + heavyDepsPatterns.forEach(pattern => { + config.plugins?.push( + new webpack.NormalModuleReplacementPlugin(pattern, nullModuleShim), + ); + }); + // Add YAML loader rule directly to existing rules config.module?.rules?.push({ test: /\.ya?ml$/, use: 'js-yaml-loader', }); + // Add babel-loader rule for superset-frontend files + // This ensures Emotion CSS-in-JS is processed correctly for SSG + const supersetFrontendPath = path.resolve( + __dirname, + '../../superset-frontend', + ); + config.module?.rules?.push({ + test: /\.(tsx?|jsx?)$/, + include: supersetFrontendPath, + use: { + loader: 'babel-loader', + options: { + presets: [ + [ + '@babel/preset-react', + { + runtime: 'automatic', + importSource: '@emotion/react', + }, + ], + '@babel/preset-typescript', + ], + plugins: ['@emotion/babel-plugin'], + }, + }, + }); + return { devtool: isDev ? 'eval-source-map' : config.devtool, ...(isDev && { @@ -44,8 +106,16 @@ export default function webpackExtendPlugin(): Plugin { }, }), resolve: { + // Add superset-frontend node_modules to module resolution + modules: [ + ...(config.resolve?.modules || []), + path.resolve(__dirname, '../../superset-frontend/node_modules'), + ], alias: { ...config.resolve.alias, + // Ensure single React instance across all modules (critical for hooks to work) + react: path.resolve(__dirname, '../node_modules/react'), + 'react-dom': path.resolve(__dirname, '../node_modules/react-dom'), // Allow importing from superset-frontend src: path.resolve(__dirname, '../../superset-frontend/src'), // '@superset-ui/core': path.resolve( @@ -58,14 +128,29 @@ export default function webpackExtendPlugin(): Plugin { __dirname, '../../superset-frontend/packages/superset-ui-core/src/components', ), - // Extension API package - allows docs to import from @apache-superset/core/ui - // This matches the established pattern used throughout the Superset codebase - // Point directly to components to avoid importing theme (which has font dependencies) - // Note: TypeScript types come from docs/src/types/apache-superset-core (see tsconfig.json) - // This split is intentional: webpack resolves actual source, tsconfig provides simplified types + // Also alias the full package path for internal imports within components + '@superset-ui/core/components': path.resolve( + __dirname, + '../../superset-frontend/packages/superset-ui-core/src/components', + ), + // Use a shim for react-table to handle CommonJS to ES module interop + // react-table v7 is CommonJS, but Superset components import it with ES module syntax + 'react-table': path.resolve(__dirname, './shims/react-table.js'), + // Extension API package - resolve @apache-superset/core and its sub-paths + // to source so the docs build doesn't depend on pre-built lib/ artifacts. + // More specific sub-path aliases must come first; webpack matches the + // longest prefix. '@apache-superset/core/ui': path.resolve( __dirname, - '../../superset-frontend/packages/superset-core/src/ui/components', + '../../superset-frontend/packages/superset-core/src/ui', + ), + '@apache-superset/core/api/core': path.resolve( + __dirname, + '../../superset-frontend/packages/superset-core/src/api/core', + ), + '@apache-superset/core': path.resolve( + __dirname, + '../../superset-frontend/packages/superset-core/src', ), // Add proper Storybook aliases '@storybook/blocks': path.resolve( diff --git a/docs/static/img/atomic-design.png b/docs/static/img/atomic-design.png new file mode 100644 index 000000000000..e44c5f34a54e Binary files /dev/null and b/docs/static/img/atomic-design.png differ diff --git a/docs/static/img/community/bluesky-symbol.svg b/docs/static/img/community/bluesky-symbol.svg new file mode 100644 index 000000000000..4c9f7945941c --- /dev/null +++ b/docs/static/img/community/bluesky-symbol.svg @@ -0,0 +1,21 @@ + + + + diff --git a/docs/static/img/community/globe-symbol.svg b/docs/static/img/community/globe-symbol.svg new file mode 100644 index 000000000000..5ee7fd386b40 --- /dev/null +++ b/docs/static/img/community/globe-symbol.svg @@ -0,0 +1,23 @@ + + + + + + diff --git a/docs/static/img/community/linkedin-symbol.svg b/docs/static/img/community/linkedin-symbol.svg new file mode 100644 index 000000000000..09a9e2202809 --- /dev/null +++ b/docs/static/img/community/linkedin-symbol.svg @@ -0,0 +1,21 @@ + + + + diff --git a/docs/static/img/community/x-symbol.svg b/docs/static/img/community/x-symbol.svg new file mode 100644 index 000000000000..d4e5a2fd3496 --- /dev/null +++ b/docs/static/img/community/x-symbol.svg @@ -0,0 +1,21 @@ + + + + diff --git a/docs/yarn.lock b/docs/yarn.lock index 5a171ac1ca04..27691d1687c2 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -312,6 +312,27 @@ json5 "^2.2.3" semver "^6.3.1" +"@babel/core@^7.26.0": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.28.5.tgz#4c81b35e51e1b734f510c99b07dfbc7bbbb48f7e" + integrity sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw== + dependencies: + "@babel/code-frame" "^7.27.1" + "@babel/generator" "^7.28.5" + "@babel/helper-compilation-targets" "^7.27.2" + "@babel/helper-module-transforms" "^7.28.3" + "@babel/helpers" "^7.28.4" + "@babel/parser" "^7.28.5" + "@babel/template" "^7.27.2" + "@babel/traverse" "^7.28.5" + "@babel/types" "^7.28.5" + "@jridgewell/remapping" "^2.3.5" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + "@babel/generator@^7.25.9", "@babel/generator@^7.28.3": version "7.28.3" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.28.3.tgz#9626c1741c650cbac39121694a0f2d7451b8ef3e" @@ -323,6 +344,17 @@ "@jridgewell/trace-mapping" "^0.3.28" jsesc "^3.0.2" +"@babel/generator@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.28.5.tgz#712722d5e50f44d07bc7ac9fe84438742dd61298" + integrity sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ== + dependencies: + "@babel/parser" "^7.28.5" + "@babel/types" "^7.28.5" + "@jridgewell/gen-mapping" "^0.3.12" + "@jridgewell/trace-mapping" "^0.3.28" + jsesc "^3.0.2" + "@babel/helper-annotate-as-pure@^7.27.1", "@babel/helper-annotate-as-pure@^7.27.3": version "7.27.3" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz#f31fd86b915fc4daf1f3ac6976c59be7084ed9c5" @@ -354,6 +386,19 @@ "@babel/traverse" "^7.28.3" semver "^6.3.1" +"@babel/helper-create-class-features-plugin@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.5.tgz#472d0c28028850968979ad89f173594a6995da46" + integrity sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.3" + "@babel/helper-member-expression-to-functions" "^7.28.5" + "@babel/helper-optimise-call-expression" "^7.27.1" + "@babel/helper-replace-supers" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + "@babel/traverse" "^7.28.5" + semver "^6.3.1" + "@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.27.1": version "7.27.1" resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.1.tgz#05b0882d97ba1d4d03519e4bce615d70afa18c53" @@ -387,6 +432,14 @@ "@babel/traverse" "^7.27.1" "@babel/types" "^7.27.1" +"@babel/helper-member-expression-to-functions@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz#f3e07a10be37ed7a63461c63e6929575945a6150" + integrity sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg== + dependencies: + "@babel/traverse" "^7.28.5" + "@babel/types" "^7.28.5" + "@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.27.1": version "7.27.1" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz#7ef769a323e2655e126673bb6d2d6913bbead204" @@ -452,6 +505,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz#a7054dcc145a967dd4dc8fee845a57c1316c9df8" integrity sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow== +"@babel/helper-validator-identifier@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4" + integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q== + "@babel/helper-validator-option@^7.27.1": version "7.27.1" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz#fa52f5b1e7db1ab049445b421c4471303897702f" @@ -474,6 +532,14 @@ "@babel/template" "^7.27.2" "@babel/types" "^7.28.2" +"@babel/helpers@^7.28.4": + version "7.28.4" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.28.4.tgz#fe07274742e95bdf7cf1443593eeb8926ab63827" + integrity sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w== + dependencies: + "@babel/template" "^7.27.2" + "@babel/types" "^7.28.4" + "@babel/parser@^7.27.2", "@babel/parser@^7.28.3": version "7.28.3" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.3.tgz#d2d25b814621bca5fe9d172bc93792547e7a2a71" @@ -481,6 +547,13 @@ dependencies: "@babel/types" "^7.28.2" +"@babel/parser@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.5.tgz#0b0225ee90362f030efd644e8034c99468893b08" + integrity sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ== + dependencies: + "@babel/types" "^7.28.5" + "@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.27.1": version "7.27.1" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz#61dd8a8e61f7eb568268d1b5f129da3eee364bf9" @@ -883,7 +956,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-react-display-name@^7.27.1": +"@babel/plugin-transform-react-display-name@^7.27.1", "@babel/plugin-transform-react-display-name@^7.28.0": version "7.28.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.28.0.tgz#6f20a7295fea7df42eb42fed8f896813f5b934de" integrity sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA== @@ -997,6 +1070,17 @@ "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" "@babel/plugin-syntax-typescript" "^7.27.1" +"@babel/plugin-transform-typescript@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.5.tgz#441c5f9a4a1315039516c6c612fc66d5f4594e72" + integrity sha512-x2Qa+v/CuEoX7Dr31iAfr0IhInrVOWZU/2vJMJ00FOR/2nM0BcBEclpaf9sWCDc+v5e9dMrhSH8/atq/kX7+bA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.3" + "@babel/helper-create-class-features-plugin" "^7.28.5" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + "@babel/plugin-syntax-typescript" "^7.27.1" + "@babel/plugin-transform-unicode-escapes@^7.27.1": version "7.27.1" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz#3e3143f8438aef842de28816ece58780190cf806" @@ -1125,6 +1209,18 @@ "@babel/plugin-transform-react-jsx-development" "^7.27.1" "@babel/plugin-transform-react-pure-annotations" "^7.27.1" +"@babel/preset-react@^7.26.3": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.28.5.tgz#6fcc0400fa79698433d653092c3919bb4b0878d9" + integrity sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-validator-option" "^7.27.1" + "@babel/plugin-transform-react-display-name" "^7.28.0" + "@babel/plugin-transform-react-jsx" "^7.27.1" + "@babel/plugin-transform-react-jsx-development" "^7.27.1" + "@babel/plugin-transform-react-pure-annotations" "^7.27.1" + "@babel/preset-typescript@^7.21.0", "@babel/preset-typescript@^7.25.9": version "7.27.1" resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.27.1.tgz#190742a6428d282306648a55b0529b561484f912" @@ -1136,6 +1232,17 @@ "@babel/plugin-transform-modules-commonjs" "^7.27.1" "@babel/plugin-transform-typescript" "^7.27.1" +"@babel/preset-typescript@^7.26.0": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz#540359efa3028236958466342967522fd8f2a60c" + integrity sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-validator-option" "^7.27.1" + "@babel/plugin-syntax-jsx" "^7.27.1" + "@babel/plugin-transform-modules-commonjs" "^7.27.1" + "@babel/plugin-transform-typescript" "^7.28.5" + "@babel/runtime-corejs3@^7.20.7", "@babel/runtime-corejs3@^7.22.15", "@babel/runtime-corejs3@^7.25.9", "@babel/runtime-corejs3@^7.26.10", "@babel/runtime-corejs3@^7.27.1": version "7.28.3" resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.28.3.tgz#8a993bea33c4f03b02b95ca9164dad26aaca125d" @@ -1170,6 +1277,19 @@ "@babel/types" "^7.28.2" debug "^4.3.1" +"@babel/traverse@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.28.5.tgz#450cab9135d21a7a2ca9d2d35aa05c20e68c360b" + integrity sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ== + dependencies: + "@babel/code-frame" "^7.27.1" + "@babel/generator" "^7.28.5" + "@babel/helper-globals" "^7.28.0" + "@babel/parser" "^7.28.5" + "@babel/template" "^7.27.2" + "@babel/types" "^7.28.5" + debug "^4.3.1" + "@babel/types@^7.21.3", "@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.28.2", "@babel/types@^7.4.4": version "7.28.2" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.2.tgz#da9db0856a9a88e0a13b019881d7513588cf712b" @@ -1178,6 +1298,14 @@ "@babel/helper-string-parser" "^7.27.1" "@babel/helper-validator-identifier" "^7.27.1" +"@babel/types@^7.28.4", "@babel/types@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.5.tgz#10fc405f60897c35f07e85493c932c7b5ca0592b" + integrity sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA== + dependencies: + "@babel/helper-string-parser" "^7.27.1" + "@babel/helper-validator-identifier" "^7.28.5" + "@braintree/sanitize-url@^7.0.4": version "7.1.1" resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-7.1.1.tgz#15e19737d946559289b915e5dad3b4c28407735e" @@ -2431,6 +2559,16 @@ resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-5.5.3.tgz#18e3af6b8eae7984072bbeb0c0858474d7c4cefe" integrity sha512-R11tGE6yIFwqpaIqcfkcg7AICXzFg14+5h5v0TfF/9+RMDL6jhzCy/pxHVOfbALGdtVYdt6JdR21tuxEgl34dw== +"@fontsource/fira-code@^5.2.7": + version "5.2.7" + resolved "https://registry.yarnpkg.com/@fontsource/fira-code/-/fira-code-5.2.7.tgz#9ecbd909d53e7196a5d895b601747fe34491fc6a" + integrity sha512-tnB9NNund9TwIym8/7DMJe573nlPEQb+fKUV5GL8TBYXjIhDvL0D7mgmNVNQUPhXp+R7RylQeiBdkA4EbOHPGQ== + +"@fontsource/inter@^5.2.8": + version "5.2.8" + resolved "https://registry.yarnpkg.com/@fontsource/inter/-/inter-5.2.8.tgz#10c95d877d972c7de5bd4592309d42fb6a5e1a5b" + integrity sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg== + "@hapi/hoek@^9.0.0", "@hapi/hoek@^9.3.0": version "9.3.0" resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb" @@ -2522,6 +2660,14 @@ "@jridgewell/sourcemap-codec" "^1.5.0" "@jridgewell/trace-mapping" "^0.3.24" +"@jridgewell/remapping@^2.3.5": + version "2.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/remapping/-/remapping-2.3.5.tgz#375c476d1972947851ba1e15ae8f123047445aa1" + integrity sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.24" + "@jridgewell/resolve-uri@^3.1.0": version "3.1.2" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" @@ -6885,9 +7031,9 @@ docusaurus-plugin-less@^2.0.2: integrity sha512-ez6WSSvGS8HoJslYHeG5SflyShWvHFXeTTHXPBd3H1T3zgq9wp6wD7scXm+rXyyfhFhP5VNiIqhYB78z4OLjwg== docusaurus-plugin-openapi-docs@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/docusaurus-plugin-openapi-docs/-/docusaurus-plugin-openapi-docs-4.6.0.tgz#2b89a3d23f1836a3691f076860dd67013b4800ab" - integrity sha512-wcRUnZca9hRiuAcw2Iz+YUVO4dh01mV2FoAtomRMVlWZIEgw6TA5SqsfHWRd6on/ibvvVS9Lq6GjZTcSjwLcWQ== + version "4.7.1" + resolved "https://registry.yarnpkg.com/docusaurus-plugin-openapi-docs/-/docusaurus-plugin-openapi-docs-4.7.1.tgz#fb1cf0d30bb49dc7ceb643ea623209bba054cb2a" + integrity sha512-RpqvTEnhIfdSuTn/Fa/8bmxeufijLL9HCRb//ELD33AKqEbCw147SKR/CqWu4H4gwi50FZLUbiHKZJbPtXLt9Q== dependencies: "@apidevtools/json-schema-ref-parser" "^11.5.4" "@redocly/openapi-core" "^1.34.3" @@ -6906,9 +7052,9 @@ docusaurus-plugin-openapi-docs@^4.6.0: xml-formatter "^3.6.6" docusaurus-theme-openapi-docs@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/docusaurus-theme-openapi-docs/-/docusaurus-theme-openapi-docs-4.6.0.tgz#d01965cef49764c861b4c4920c363ac4bf88cb82" - integrity sha512-YCgYReVMcrKDTNvM4dh9+i+ies+sGbCwv12TRCPZZbeif7RqTc/5w4rhxEIfp/v0uOAQGL4iXfTSBAMExotbMQ== + version "4.7.1" + resolved "https://registry.yarnpkg.com/docusaurus-theme-openapi-docs/-/docusaurus-theme-openapi-docs-4.7.1.tgz#bcdb59a76852ed5f9dc77280b38e62de0745d699" + integrity sha512-OPydf11LoEY3fdxaoqCVO+qCk7LBo6l6s28UvHJ5mIN/2xu+dOOio9+xnKZ5FIPOlD+dx0gVSKzaVCi/UFTxlg== dependencies: "@hookform/error-message" "^2.0.1" "@reduxjs/toolkit" "^2.8.2" @@ -12475,6 +12621,13 @@ react-redux@^9.2.0: "@types/use-sync-external-store" "^0.0.6" use-sync-external-store "^1.4.0" +react-resize-detector@7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/react-resize-detector/-/react-resize-detector-7.1.2.tgz#8ef975dd8c3d56f9a5160ac382ef7136dcd2d86c" + integrity sha512-zXnPJ2m8+6oq9Nn8zsep/orts9vQv3elrpA+R8XTcW7DVVUJ9vwDwMXaBtykAYjMnkCIaOoK9vObyR7ZgFNlOw== + dependencies: + lodash "^4.17.21" + react-router-config@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/react-router-config/-/react-router-config-5.1.1.tgz#0f4263d1a80c6b2dc7b9c1902c9526478194a988" @@ -12530,6 +12683,11 @@ react-syntax-highlighter@^16.0.0: prismjs "^1.30.0" refractor "^5.0.0" +react-table@^7.8.0: + version "7.8.0" + resolved "https://registry.yarnpkg.com/react-table/-/react-table-7.8.0.tgz#07858c01c1718c09f7f1aed7034fcfd7bda907d2" + integrity sha512-hNaz4ygkZO4bESeFfnfOft73iBUj8K5oKi1EcSHPAibEydfsX2MyU6Z8KCr3mv3C9Kqqh71U+DhZkFvibbnPbA== + "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0": version "19.1.1" resolved "https://registry.yarnpkg.com/react/-/react-19.1.1.tgz#06d9149ec5e083a67f9a1e39ce97b06a03b644af" diff --git a/superset-frontend/packages/superset-ui-core/src/components/AutoComplete/AutoComplete.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/AutoComplete/AutoComplete.stories.tsx index 1b465ff15277..3039d8bdfa3e 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/AutoComplete/AutoComplete.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/AutoComplete/AutoComplete.stories.tsx @@ -217,6 +217,15 @@ export default { }, } as Meta; +// Static options for docs and simple demos +const staticOptions = [ + { value: 'Dashboard', label: 'Dashboard' }, + { value: 'Chart', label: 'Chart' }, + { value: 'Dataset', label: 'Dataset' }, + { value: 'Database', label: 'Database' }, + { value: 'Query', label: 'Query' }, +]; + const getRandomInt = (max: number, min = 0) => Math.floor(Math.random() * (max - min + 1)) + min; @@ -243,7 +252,7 @@ const searchResult = (query: string) => }; }); -const AutoCompleteWithOptions = (args: AutoCompleteProps) => { +const AutoCompleteWithDynamicSearch = (args: AutoCompleteProps) => { const [options, setOptions] = useState([]); const handleSearch = (value: string) => { @@ -252,16 +261,60 @@ const AutoCompleteWithOptions = (args: AutoCompleteProps) => { return ; }; + type Story = StoryObj; -export const AutoCompleteStory: Story = { +// Interactive story with static options - works in both Storybook and Docs +export const InteractiveAutoComplete: Story = { + args: { + style: { width: 300 }, + placeholder: 'Type to search...', + options: staticOptions, + filterOption: true, // Enable built-in filtering for static options + }, + argTypes: { + options: { + control: false, + description: 'The dropdown options', + }, + filterOption: { + control: 'boolean', + description: 'Enable filtering of options based on input', + }, + }, +}; + +// Docs configuration - provides static options for documentation rendering +InteractiveAutoComplete.parameters = { + docs: { + staticProps: { + options: [ + { value: 'Dashboard', label: 'Dashboard' }, + { value: 'Chart', label: 'Chart' }, + { value: 'Dataset', label: 'Dataset' }, + { value: 'Database', label: 'Database' }, + { value: 'Query', label: 'Query' }, + ], + style: { width: 300 }, + filterOption: true, + }, + }, +}; + +// Dynamic search demo - Storybook only (uses render function) +export const DynamicSearchDemo: Story = { args: { style: { width: 300 }, placeholder: 'Type to search...', }, render: (args: AutoCompleteProps) => (
- +
), + parameters: { + docs: { + disable: true, // Hide from docs, it's a Storybook-specific demo + }, + }, }; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Avatar/Avatar.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Avatar/Avatar.stories.tsx index 5122cd8b51ac..43ba70c755e9 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Avatar/Avatar.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Avatar/Avatar.stories.tsx @@ -27,6 +27,7 @@ export default { export const InteractiveAvatar = (args: AvatarProps) => ; InteractiveAvatar.args = { + children: 'AB', alt: '', gap: 4, shape: 'circle', @@ -36,8 +37,26 @@ InteractiveAvatar.args = { }; InteractiveAvatar.argTypes = { + children: { + description: 'Text or initials to display inside the avatar.', + control: { type: 'text' }, + }, shape: { + description: 'The shape of the avatar.', options: ['circle', 'square'], control: { type: 'select' }, }, + size: { + description: 'The size of the avatar.', + options: ['small', 'default', 'large'], + control: { type: 'select' }, + }, + src: { + description: 'Image URL for the avatar. If provided, overrides children.', + control: { type: 'text' }, + }, + gap: { + description: 'Letter spacing inside the avatar.', + control: { type: 'number', min: 0, max: 10 }, + }, }; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Badge/Badge.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Badge/Badge.stories.tsx index 9a5ff53d7375..497b202e352e 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Badge/Badge.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Badge/Badge.stories.tsx @@ -45,7 +45,6 @@ const badgeColors: BadgeColorValue[] = [ 'lime', ]; const badgeSizes: BadgeSizeValue[] = ['default', 'small']; -const STATUSES = ['default', 'error', 'warning', 'success', 'processing']; const COLORS = { label: 'colors', @@ -59,55 +58,119 @@ const SIZES = { defaultValue: undefined, }; -export const InteractiveBadge = (args: BadgeProps) => ; +// Count Badge - shows a number +export const InteractiveBadge = (args: BadgeProps) => ( + +
+ +); InteractiveBadge.args = { - count: undefined, - color: undefined, - text: 'Text', - status: 'success', + count: 5, size: 'default', showZero: false, overflowCount: 99, }; InteractiveBadge.argTypes = { - status: { - control: { - type: 'select', - }, - options: [undefined, ...STATUSES], - description: - 'only works if `count` is `undefined` (or is set to 0) and `color` is set to `undefined`', + count: { + description: 'Number to show in the badge.', + control: { type: 'number' }, }, size: { - control: { - type: 'select', - }, - options: SIZES.options, + description: 'Size of the badge.', + control: { type: 'select' }, + options: ['default', 'small'], }, color: { - control: { - type: 'select', - }, - options: [undefined, ...COLORS.options], - }, - count: { - control: { - type: 'select', - defaultValue: undefined, - }, - options: [undefined, ...Array(100).keys()], - defaultValue: undefined, + description: 'Custom background color for the badge.', + control: { type: 'select' }, + options: [ + 'pink', + 'red', + 'yellow', + 'orange', + 'cyan', + 'green', + 'blue', + 'purple', + 'geekblue', + 'magenta', + 'volcano', + 'gold', + 'lime', + ], }, showZero: { - control: 'boolean', - defaultValue: false, + description: 'Whether to show badge when count is zero.', + control: { type: 'boolean' }, }, overflowCount: { - control: 'number', - description: - 'The threshold at which the number overflows with a `+` e.g if you set this to 10, and the value is 11, you get `11+`', + description: 'Max count to show. Shows count+ when exceeded (e.g., 99+).', + control: { type: 'number' }, + }, +}; + +InteractiveBadge.parameters = { + docs: { + description: { + story: 'Badge can show a count number or a status indicator dot.', + }, + examples: [ + { + title: 'Status Badge', + code: `function StatusBadgeDemo() { + const statuses = ['default', 'success', 'processing', 'warning', 'error']; + return ( +
+ {statuses.map(status => ( + + ))} +
+ ); +}`, + }, + { + title: 'Color Gallery', + code: `function ColorGallery() { + const colors = ['pink', 'red', 'orange', 'green', 'cyan', 'blue', 'purple']; + return ( +
+ {colors.map(color => ( + + ))} +
+ ); +}`, + }, + ], + }, +}; + +// Status Badge - shows a status dot with text +export const StatusBadge = (args: BadgeProps) => ; + +StatusBadge.args = { + status: 'success', + text: 'Completed', +}; + +StatusBadge.argTypes = { + status: { + description: 'Status type for the badge dot.', + control: { type: 'select' }, + options: ['default', 'error', 'warning', 'success', 'processing'], + }, + text: { + description: 'Text to display next to the status dot.', + control: { type: 'text' }, }, }; @@ -116,14 +179,16 @@ export const BadgeGallery = () => ( {SIZES.options.map(size => (

{size}

- {COLORS.options.map(color => ( - - ))} +
+ {COLORS.options.map(color => ( + + ))} +
))} diff --git a/superset-frontend/packages/superset-ui-core/src/components/Breadcrumb/Breadcrumb.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Breadcrumb/Breadcrumb.stories.tsx index 3cd070d34ca1..52b11b4b4d1e 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Breadcrumb/Breadcrumb.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Breadcrumb/Breadcrumb.stories.tsx @@ -18,6 +18,13 @@ */ import type { Meta, StoryObj } from '@storybook/react'; import { Breadcrumb } from '.'; +import type { BreadcrumbProps } from './types'; + +const sampleItems = [ + { title: 'Home', href: '/' }, + { title: 'Library', href: '/library' }, + { title: 'Data' }, +]; export default { title: 'Components/Breadcrumb', @@ -33,16 +40,16 @@ export default { }, items: { control: false, - description: 'List of breadcrumb items', + description: 'Array of breadcrumb items with title and optional href', table: { - type: { summary: 'object' }, + type: { summary: '{ title: string, href?: string }[]' }, }, }, }, parameters: { docs: { description: { - component: 'Breadcrumb component for displaying navigation paths', + component: 'Breadcrumb component for displaying navigation paths.', }, }, }, @@ -50,13 +57,55 @@ export default { type Story = StoryObj; +export const InteractiveBreadcrumb = (args: BreadcrumbProps) => ( + +); + +InteractiveBreadcrumb.args = { + items: sampleItems, + separator: '/', +}; + +InteractiveBreadcrumb.argTypes = { + separator: { + description: 'Custom separator between items.', + control: 'text', + }, + items: { + description: 'Array of breadcrumb items with title and optional href.', + control: false, + }, +}; + +InteractiveBreadcrumb.parameters = { + docs: { + staticProps: { + items: [ + { title: 'Home', href: '/' }, + { title: 'Library', href: '/library' }, + { title: 'Data' }, + ], + separator: '/', + }, + liveExample: `function Demo() { + return ( + + ); +}`, + }, +}; + +// Keep original for backwards compatibility export const Default: Story = { args: { - items: [ - { title: 'Home', href: '/' }, - { title: 'Library', href: '/library' }, - { title: 'Data' }, - ], + items: sampleItems, }, render: args => , }; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Button/Button.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Button/Button.stories.tsx index 4f3b7275cca9..ede9d87cbb88 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Button/Button.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Button/Button.stories.tsx @@ -100,18 +100,31 @@ ButtonGallery.parameters = { }, }; -export const InteractiveButton = (args: ButtonProps & { label: string }) => { - const { label, ...btnArgs } = args; - return ; -}; +export const InteractiveButton = (args: ButtonProps & { children: string }) => ( + + + + +); + +InteractiveButtonGroup.args = { + expand: false, +}; + +InteractiveButtonGroup.argTypes = { + expand: { + description: 'When true, buttons expand to fill available width.', + control: 'boolean', + }, + className: { + description: 'CSS class name for custom styling.', + control: 'text', + }, + children: { + description: 'Button components to render inside the group.', + control: false, + }, +}; + +InteractiveButtonGroup.parameters = { + actions: { + disable: true, + }, + docs: { + staticProps: { + expand: false, + }, + sampleChildren: [ + { + component: 'Button', + props: { buttonStyle: 'tertiary', children: 'Button 1' }, + }, + { + component: 'Button', + props: { buttonStyle: 'tertiary', children: 'Button 2' }, + }, + { + component: 'Button', + props: { buttonStyle: 'tertiary', children: 'Button 3' }, + }, + ], + liveExample: `function Demo() { + return ( + + + + + + ); +}`, + }, +}; + +// Gallery showing different button styles in groups +export const ButtonGroupGallery = (args: ButtonProps) => ( <> @@ -42,25 +113,34 @@ export const InteractiveButtonGroup = (args: ButtonProps) => ( ); -InteractiveButtonGroup.args = { + +ButtonGroupGallery.args = { buttonStyle: 'tertiary', buttonSize: 'default', }; -InteractiveButtonGroup.argTypes = { +ButtonGroupGallery.argTypes = { buttonStyle: { - name: STYLES.label, + description: 'Style variant for the buttons.', control: { type: 'select' }, - options: STYLES.options, + options: [ + 'primary', + 'secondary', + 'tertiary', + 'dashed', + 'link', + 'warning', + 'danger', + ], }, buttonSize: { - name: SIZES.label, + description: 'Size of the buttons.', control: { type: 'select' }, - options: SIZES.options, + options: ['default', 'small', 'xsmall'], }, }; -InteractiveButtonGroup.parameters = { +ButtonGroupGallery.parameters = { actions: { disable: true, }, diff --git a/superset-frontend/packages/superset-ui-core/src/components/Card/Card.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Card/Card.stories.tsx index 3009da49b92b..34775c6c8a2e 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Card/Card.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Card/Card.stories.tsx @@ -22,29 +22,94 @@ import type { CardProps } from './types'; export default { title: 'Components/Card', component: Card, + parameters: { + docs: { + description: { + component: + 'A container component for grouping related content. ' + + 'Supports titles, borders, loading states, and hover effects.', + }, + }, + }, }; export const InteractiveCard = (args: CardProps) => ; InteractiveCard.args = { padded: true, - title: 'Components/Card', - children: 'Card content', + title: 'Dashboard Overview', + children: + 'This card displays a summary of your dashboard metrics and recent activity.', bordered: true, loading: false, hoverable: false, }; InteractiveCard.argTypes = { + padded: { + control: { type: 'boolean' }, + description: 'Whether the card content has padding.', + }, + title: { + control: { type: 'text' }, + description: 'Title text displayed at the top of the card.', + }, + children: { + control: { type: 'text' }, + description: 'The content inside the card.', + }, + bordered: { + control: { type: 'boolean' }, + description: 'Whether to show a border around the card.', + }, + loading: { + control: { type: 'boolean' }, + description: 'Whether to show a loading skeleton.', + }, + hoverable: { + control: { type: 'boolean' }, + description: 'Whether the card lifts on hover.', + }, onClick: { - table: { - disable: true, - }, + table: { disable: true }, action: 'onClick', }, theme: { - table: { - disable: true, - }, + table: { disable: true }, + }, +}; + +InteractiveCard.parameters = { + docs: { + liveExample: `function Demo() { + return ( + + This card displays a summary of your dashboard metrics and recent activity. + + ); +}`, + examples: [ + { + title: 'Card States', + code: `function CardStates() { + return ( +
+ + Default card content. + + + Hover over this card. + + + This content is hidden while loading. + + + Borderless card. + +
+ ); +}`, + }, + ], }, }; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Checkbox/Checkbox.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Checkbox/Checkbox.stories.tsx index afa3c211555e..cae77987c5f3 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Checkbox/Checkbox.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Checkbox/Checkbox.stories.tsx @@ -67,6 +67,75 @@ InteractiveCheckbox.args = { indeterminate: false, }; +InteractiveCheckbox.argTypes = { + checked: { + control: { type: 'boolean' }, + description: 'Whether the checkbox is checked.', + }, + indeterminate: { + control: { type: 'boolean' }, + description: + 'Whether the checkbox is in indeterminate state (partially selected).', + }, +}; + +InteractiveCheckbox.parameters = { + docs: { + examples: [ + { + title: 'All Checkbox States', + code: `function AllStates() { + return ( +
+ Unchecked + Checked + Indeterminate + Disabled unchecked + Disabled checked +
+ ); +}`, + }, + { + title: 'Select All Pattern', + code: `function SelectAllDemo() { + const [selected, setSelected] = React.useState([]); + const options = ['Option A', 'Option B', 'Option C']; + + const allSelected = selected.length === options.length; + const indeterminate = selected.length > 0 && !allSelected; + + return ( +
+ setSelected(e.target.checked ? [...options] : [])} + > + Select All + +
+ {options.map(opt => ( +
+ setSelected(prev => + prev.includes(opt) ? prev.filter(x => x !== opt) : [...prev, opt] + )} + > + {opt} + +
+ ))} +
+
+ ); +}`, + }, + ], + }, +}; + // All checkbox states including indeterminate const STATES = [ { diff --git a/superset-frontend/packages/superset-ui-core/src/components/DatePicker/DatePicker.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/DatePicker/DatePicker.stories.tsx index 6043effffb5e..1168ae69a9ad 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/DatePicker/DatePicker.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/DatePicker/DatePicker.stories.tsx @@ -80,11 +80,51 @@ export const InteractiveDatePicker: any = (args: DatePickerProps) => ( InteractiveDatePicker.args = { ...commonArgs, placeholder: 'Placeholder', - showToday: true, + showNow: true, showTime: { format: 'hh:mm a', needConfirm: false }, }; -InteractiveDatePicker.argTypes = interactiveTypes; +InteractiveDatePicker.argTypes = { + ...interactiveTypes, + showNow: { + description: 'Show "Now" button to select current date and time.', + control: 'boolean', + }, +}; + +InteractiveDatePicker.parameters = { + actions: { + disable: true, + }, + docs: { + description: { + story: 'A date picker component with time selection support.', + }, + staticProps: { + allowClear: false, + autoFocus: true, + disabled: false, + format: 'YYYY-MM-DD hh:mm a', + inputReadOnly: false, + picker: 'date', + placement: 'bottomLeft', + size: 'middle', + showNow: true, + placeholder: 'Select date', + showTime: { format: 'hh:mm a', needConfirm: false }, + }, + liveExample: `function Demo() { + return ( + + ); +}`, + }, +}; export const InteractiveRangePicker = ( args: Omit & { diff --git a/superset-frontend/packages/superset-ui-core/src/components/Divider/Divider.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Divider/Divider.stories.tsx index 903fa3b238b2..e48ab78a6cd3 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Divider/Divider.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Divider/Divider.stories.tsx @@ -30,6 +30,7 @@ InteractiveDivider.args = { dashed: false, variant: 'solid', orientation: 'center', + orientationMargin: '', plain: true, type: 'horizontal', }; @@ -38,16 +39,56 @@ InteractiveDivider.argTypes = { variant: { control: { type: 'select' }, options: ['dashed', 'dotted', 'solid'], + description: 'Line style of the divider.', }, orientation: { control: { type: 'select' }, options: ['left', 'right', 'center'], + description: 'Position of title inside divider.', }, orientationMargin: { control: { type: 'text' }, + description: 'Margin from divider edge to title.', }, type: { control: { type: 'select' }, options: ['horizontal', 'vertical'], + description: 'Direction of the divider.', + }, + dashed: { + description: 'Whether line is dashed (deprecated, use variant).', + }, + plain: { + description: 'Use plain style without bold title.', + }, +}; + +InteractiveDivider.parameters = { + actions: { + disable: true, + }, + docs: { + description: { + story: + 'A divider line to separate content. Use horizontal for sections, vertical for inline elements.', + }, + liveExample: `function Demo() { + return ( + <> +

Horizontal divider with title (orientationMargin applies here):

+ Left Title + Right Title + Center Title +

Vertical divider (use container gap for spacing):

+
+ Link + + Link + + Link +
+ + ); +}`, }, }; diff --git a/superset-frontend/packages/superset-ui-core/src/components/DropdownContainer/DropdownContainer.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/DropdownContainer/DropdownContainer.stories.tsx index 7a92df392df8..663436ba8fe2 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/DropdownContainer/DropdownContainer.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/DropdownContainer/DropdownContainer.stories.tsx @@ -27,6 +27,14 @@ import { DropdownContainer } from '.'; export default { title: 'Design System/Components/DropdownContainer', component: DropdownContainer, + parameters: { + docs: { + description: { + component: + 'DropdownContainer arranges items horizontally and moves overflowing items into a dropdown popover. Resize the container to see the overflow behavior.', + }, + }, + }, }; const ITEMS_COUNT = 6; @@ -108,3 +116,134 @@ export const Component = (props: DropdownContainerProps) => {
); }; + +// Interactive story for docs generation +export const InteractiveDropdownContainer = (args: DropdownContainerProps) => { + const simpleItems = Array.from({ length: 6 }, (_, i) => ({ + id: `item-${i}`, + element: ( +
+ Filter {i + 1} +
+ ), + })); + return ( +
+ +
+ ); +}; + +InteractiveDropdownContainer.args = {}; + +InteractiveDropdownContainer.argTypes = {}; + +InteractiveDropdownContainer.parameters = { + docs: { + staticProps: { + style: { maxWidth: 360 }, + items: [ + { + id: 'item-0', + element: { + component: 'Tag', + props: { children: 'Region', color: 'blue' }, + }, + }, + { + id: 'item-1', + element: { + component: 'Tag', + props: { children: 'Category', color: 'blue' }, + }, + }, + { + id: 'item-2', + element: { + component: 'Tag', + props: { children: 'Date Range', color: 'blue' }, + }, + }, + { + id: 'item-3', + element: { + component: 'Tag', + props: { children: 'Status', color: 'blue' }, + }, + }, + { + id: 'item-4', + element: { + component: 'Tag', + props: { children: 'Owner', color: 'blue' }, + }, + }, + { + id: 'item-5', + element: { + component: 'Tag', + props: { children: 'Priority', color: 'blue' }, + }, + }, + ], + }, + liveExample: `function Demo() { + const items = Array.from({ length: 6 }, (_, i) => ({ + id: 'item-' + i, + element: React.createElement('div', { + style: { + minWidth: 120, + padding: '4px 12px', + background: '#e6f4ff', + border: '1px solid #91caff', + borderRadius: 4, + }, + }, 'Filter ' + (i + 1)), + })); + return ( +
+ +
+ Drag the right edge to resize and see items overflow into a dropdown +
+
+ ); +}`, + examples: [ + { + title: 'With Select Filters', + code: `function SelectFilters() { + const items = ['Region', 'Category', 'Date Range', 'Status', 'Owner'].map( + (label, i) => ({ + id: 'filter-' + i, + element: React.createElement('div', { + style: { minWidth: 150, padding: '4px 12px', background: '#f5f5f5', border: '1px solid #d9d9d9', borderRadius: 4 }, + }, label + ': All'), + }) + ); + return ( +
+ +
+ ); +}`, + }, + ], + }, +}; diff --git a/superset-frontend/packages/superset-ui-core/src/components/EditableTitle/EditableTitle.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/EditableTitle/EditableTitle.stories.tsx index e778e71ed4e1..bcadf53679dc 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/EditableTitle/EditableTitle.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/EditableTitle/EditableTitle.stories.tsx @@ -37,10 +37,71 @@ InteractiveEditableTitle.args = { title: 'Title', defaultTitle: 'Default title', placeholder: 'Placeholder', + certifiedBy: '', + certificationDetails: '', maxWidth: 100, autoSize: true, }; InteractiveEditableTitle.argTypes = { + canEdit: { + description: 'Whether the title can be edited.', + }, + editing: { + description: 'Whether the title is currently in edit mode.', + }, + emptyText: { + description: 'Text to display when title is empty.', + }, + noPermitTooltip: { + description: 'Tooltip shown when user lacks edit permission.', + }, + showTooltip: { + description: 'Whether to show tooltip on hover.', + }, + title: { + description: 'The title text to display.', + }, + defaultTitle: { + description: 'Default title when none is provided.', + }, + placeholder: { + description: 'Placeholder text when editing.', + }, + certifiedBy: { + description: 'Name of person/team who certified this item.', + }, + certificationDetails: { + description: 'Additional certification details or description.', + }, + maxWidth: { + description: 'Maximum width of the title in pixels.', + }, + autoSize: { + description: 'Whether to auto-size based on content.', + }, onSaveTitle: { action: 'onSaveTitle' }, }; + +InteractiveEditableTitle.parameters = { + actions: { + disable: true, + }, + docs: { + description: { + story: 'An editable title component with optional certification badge.', + }, + liveExample: `function Demo() { + return ( + console.log('Saved:', newTitle)} + /> + ); +}`, + }, +}; diff --git a/superset-frontend/packages/superset-ui-core/src/components/EmptyState/EmptyState.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/EmptyState/EmptyState.stories.tsx index 2c58d7ca4597..d2c11bef7698 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/EmptyState/EmptyState.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/EmptyState/EmptyState.stories.tsx @@ -33,14 +33,6 @@ const emptyStates = [ export default { title: 'Components/EmptyState', component: EmptyState, - argTypes: { - size: { - control: { type: 'select' }, - options: ['small', 'medium', 'large'], - defaultValue: 'medium', - description: 'Size of the Empty State components', - }, - }, } as Meta; export const Gallery: StoryFn<{ size: 'small' | 'medium' | 'large' }> = ({ @@ -65,3 +57,117 @@ export const Gallery: StoryFn<{ size: 'small' | 'medium' | 'large' }> = ({ Gallery.args = { size: 'medium', }; + +Gallery.argTypes = { + size: { + control: { type: 'select' }, + options: ['small', 'medium', 'large'], + description: 'Size of the Empty State components', + }, +}; + +// Interactive story for docs +export const InteractiveEmptyState: StoryFn<{ + size: 'small' | 'medium' | 'large'; + title: string; + description: string; + image: string; + buttonText: string; +}> = args => ; + +InteractiveEmptyState.args = { + size: 'medium', + title: 'No Data Available', + description: 'There is no data to display at this time.', + image: 'empty.svg', + buttonText: '', +}; + +InteractiveEmptyState.argTypes = { + size: { + control: { type: 'select' }, + options: ['small', 'medium', 'large'], + description: 'Size of the empty state component.', + }, + title: { + control: { type: 'text' }, + description: 'Main title text.', + }, + description: { + control: { type: 'text' }, + description: 'Description text below the title.', + }, + image: { + control: { type: 'select' }, + options: [ + 'chart.svg', + 'document.svg', + 'empty-charts.svg', + 'empty-dashboard.svg', + 'empty-dataset.svg', + 'empty-query.svg', + 'empty-table.svg', + 'empty.svg', + 'empty_sql_chart.svg', + 'filter-results.svg', + 'filter.svg', + 'star-circle.svg', + 'union.svg', + 'vector.svg', + ], + description: 'Predefined image to display.', + }, + buttonText: { + control: { type: 'text' }, + description: 'Text for optional action button.', + }, +}; + +// All available image keys for gallery +const imageKeys = [ + 'chart.svg', + 'document.svg', + 'empty-charts.svg', + 'empty-dashboard.svg', + 'empty-dataset.svg', + 'empty-query.svg', + 'empty-table.svg', + 'empty.svg', + 'empty_sql_chart.svg', + 'filter-results.svg', + 'filter.svg', + 'star-circle.svg', + 'union.svg', + 'vector.svg', +]; + +// Single size for gallery display +const gallerySizes = ['medium']; + +InteractiveEmptyState.parameters = { + docs: { + description: { + story: + 'A component for displaying empty states with optional images and actions.', + }, + gallery: { + component: 'EmptyState', + sizes: gallerySizes, + styles: imageKeys, + sizeProp: 'size', + styleProp: 'image', + }, + liveExample: `function Demo() { + return ( + alert('Filters cleared!')} + /> + ); +}`, + }, +}; diff --git a/superset-frontend/packages/superset-ui-core/src/components/FaveStar/FaveStar.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/FaveStar/FaveStar.stories.tsx index 6834c4495c2b..07581cc03cf8 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/FaveStar/FaveStar.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/FaveStar/FaveStar.stories.tsx @@ -71,3 +71,46 @@ export const Default: Story = {
), }; + +export const InteractiveFaveStar: Story = { + args: { + itemId: 1, + isStarred: false, + showTooltip: true, + saveFaveStar: () => {}, + }, + argTypes: { + isStarred: { + control: 'boolean', + description: 'Whether the item is currently starred.', + }, + showTooltip: { + control: 'boolean', + description: 'Show tooltip on hover.', + }, + }, + render: args => ( + + + + ), + parameters: { + docs: { + description: { + story: 'A star icon for marking items as favorites.', + }, + liveExample: `function Demo() { + const [starred, setStarred] = React.useState(false); + const toggle = React.useCallback(() => setStarred(prev => !prev), []); + return ( + + ); +}`, + }, + }, +}; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Flex/Flex.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Flex/Flex.stories.tsx index 553a6603b7de..5ba2311a73a9 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Flex/Flex.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Flex/Flex.stories.tsx @@ -26,6 +26,17 @@ export default { component: Flex, }; +// Sample children used in both Storybook and auto-generated docs +const SAMPLE_ITEMS = ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5']; + +// Shared styling for sample items - matches docs site rendering +const sampleItemStyle = { + padding: '8px 16px', + background: '#e6f4ff', + border: '1px solid #91caff', + borderRadius: '4px', +}; + export const InteractiveFlex = (args: FlexProps) => ( ( height: 90vh; `} > - {new Array(20).fill(null).map((_, i) => ( -

Item

+ {SAMPLE_ITEMS.map((item, i) => ( +
+ {item} +
))}
); @@ -85,3 +98,80 @@ InteractiveFlex.argTypes = { type: { name: 'string', required: false }, }, }; + +InteractiveFlex.parameters = { + docs: { + // Inline for the static parser (can't resolve variable references) + sampleChildren: ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5'], + sampleChildrenStyle: { + padding: '8px 16px', + background: '#e6f4ff', + border: '1px solid #91caff', + borderRadius: '4px', + }, + liveExample: `function Demo() { + return ( + + {['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5'].map(item => ( +
+ {item} +
+ ))} +
+ ); +}`, + examples: [ + { + title: 'Vertical Layout', + code: `function VerticalFlex() { + return ( + + + + + + ); +}`, + }, + { + title: 'Justify and Align', + code: `function JustifyAlign() { + const boxStyle = { + width: '100%', + height: 120, + borderRadius: 6, + border: '1px solid #40a9ff', + }; + const itemStyle = { + width: 60, + height: 40, + backgroundColor: '#1677ff', + borderRadius: 4, + }; + return ( +
+ {['flex-start', 'center', 'flex-end', 'space-between', 'space-around'].map(justify => ( +
+ {justify} + +
+
+
+ +
+ ))} +
+ ); +}`, + }, + ], + }, +}; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Grid/Grid.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Grid/Grid.stories.tsx index 0a09f703ddba..e08ff73f6b03 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Grid/Grid.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Grid/Grid.stories.tsx @@ -26,18 +26,30 @@ export default { title: 'Design System/Components/Grid', component: Row, subcomponents: { Col }, + parameters: { + docs: { + description: { + component: + 'The Grid system of Ant Design is based on a 24-grid layout. The `Row` and `Col` components are used to create flexible and responsive grid layouts.', + }, + }, + }, +} as Meta; + +type Story = StoryObj; + +export const InteractiveGrid: Story = { + args: { + align: 'top', + justify: 'start', + wrap: true, + gutter: 16, + }, argTypes: { - // Row properties align: { control: 'select', options: ['top', 'middle', 'bottom', 'stretch'], - description: 'Vertical alignment of flex items.', - defaultValue: 'top', - table: { - category: 'Row', - type: { summary: 'string' }, - defaultValue: { summary: 'top' }, - }, + description: 'Vertical alignment of columns within the row.', }, justify: { control: 'select', @@ -49,161 +61,207 @@ export default { 'space-between', 'space-evenly', ], - description: 'Horizontal arrangement of flex items.', - defaultValue: undefined, - table: { - category: 'Row', - type: { summary: 'string' }, - defaultValue: { summary: 'start' }, - }, - }, - gutter: { - control: false, - description: 'Spacing between grids (horizontal and vertical).', - defaultValue: 0, - table: { - category: 'Row', - type: { summary: 'number | object | array' }, - defaultValue: { summary: '0' }, - }, + description: 'Horizontal distribution of columns within the row.', }, wrap: { control: 'boolean', - description: 'Whether the flex container is allowed to wrap its items.', - defaultValue: true, - table: { - category: 'Row', - type: { summary: 'boolean' }, - defaultValue: { summary: 'true' }, - }, - }, - // Col properties - span: { - control: 'number', - description: 'Number of grid columns to span.', - defaultValue: 24, - table: { - category: 'Col', - type: { summary: 'number' }, - defaultValue: { summary: 24 }, - }, - }, - offset: { - control: 'number', - description: 'Number of grid columns to offset from the left.', - defaultValue: 0, - table: { - category: 'Col', - type: { summary: 'number' }, - defaultValue: { summary: 0 }, - }, - }, - order: { - control: 'number', - description: 'Flex order style of the grid column.', - defaultValue: 0, - table: { - category: 'Col', - type: { summary: 'number' }, - defaultValue: { summary: 0 }, - }, - }, - pull: { - control: 'number', - description: 'Number of grid columns to pull to the left.', - defaultValue: 0, - table: { - category: 'Col', - type: { summary: 'number' }, - defaultValue: { summary: 0 }, - }, - }, - push: { - control: 'number', - description: 'Number of grid columns to push to the right.', - defaultValue: 0, - table: { - category: 'Col', - type: { summary: 'number' }, - defaultValue: { summary: 0 }, - }, - }, - flex: { - control: 'text', - description: 'Flex layout style for the column.', - table: { - category: 'Col', - type: { summary: 'string | number' }, - }, + description: 'Whether columns are allowed to wrap to the next line.', }, - // Responsive properties (xs, sm, md, etc.) - xs: { - control: 'number', - description: - 'Settings for extra small screens (< 576px). Can be a number (span) or object.', - table: { - category: 'Col', - type: { summary: 'number | object' }, - }, - }, - sm: { + gutter: { control: 'number', - description: - 'Settings for small screens (≥ 576px). Can be a number (span) or object.', - table: { - category: 'Col', - type: { summary: 'number | object' }, - }, + description: 'Spacing between columns in pixels.', }, - md: { - control: 'number', - description: - 'Settings for medium screens (≥ 768px). Can be a number (span) or object.', - table: { - category: 'Col', - type: { summary: 'number | object' }, + }, + render: ({ align, justify, wrap, ...rest }: RowProps & ColProps) => { + const [gutter, setGutter] = useState(24); + const [vgutter, setVgutter] = useState(24); + const [colCount, setColCount] = useState(4); + const rowProps = { align, justify, wrap }; + const colProps = rest; + + const cols = Array.from({ length: colCount }, (_, i) => ( +
+ Column {i + 1} + + )); + + return ( +
+
+ Horizontal Gutter: + +
+
+ Vertical Gutter: + +
+
+ Column Count: + +
+ + {cols} + +
+ ); + }, +}; + +InteractiveGrid.parameters = { + docs: { + renderComponent: 'Row', + sampleChildren: [ + { + component: 'Col', + props: { + span: 4, + children: 'col-4', + style: { + background: '#e6f4ff', + padding: '8px', + border: '1px solid #91caff', + textAlign: 'center', + }, + }, }, - }, - lg: { - control: 'number', - description: - 'Settings for large screens (≥ 992px). Can be a number (span) or object.', - table: { - category: 'Col', - type: { summary: 'number | object' }, + { + component: 'Col', + props: { + span: 4, + children: 'col-4 (tall)', + style: { + background: '#e6f4ff', + padding: '24px 8px', + border: '1px solid #91caff', + textAlign: 'center', + }, + }, }, - }, - xl: { - control: 'number', - description: - 'Settings for extra-large screens (≥ 1200px). Can be a number (span) or object.', - table: { - category: 'Col', - type: { summary: 'number | object' }, + { + component: 'Col', + props: { + span: 4, + children: 'col-4', + style: { + background: '#e6f4ff', + padding: '8px', + border: '1px solid #91caff', + textAlign: 'center', + }, + }, }, + ], + description: { + story: + 'Grid layout system based on 24 columns with configurable gutters.', }, - xxl: { - control: 'number', - description: - 'Settings for extra-extra-large screens (≥ 1600px). Can be a number (span) or object.', - table: { - category: 'Col', - type: { summary: 'number | object' }, + liveExample: `function Demo() { + return ( + + +
col-12
+ + +
col-12
+ + +
col-8
+ + +
col-8
+ + +
col-8
+ + + ); +}`, + examples: [ + { + title: 'Responsive Grid', + code: `function ResponsiveGrid() { + return ( + + +
+ Responsive +
+ + +
+ Responsive +
+ + +
+ Responsive +
+ + +
+ Responsive +
+ + + ); +}`, }, - }, - }, - parameters: { - docs: { - description: { - component: - 'The Grid system of Ant Design is based on a 24-grid layout. The `Row` and `Col` components are used to create flexible and responsive grid layouts.', + { + title: 'Alignment', + code: `function AlignmentDemo() { + const boxStyle = { background: '#e6f4ff', padding: '16px 0', border: '1px solid #91caff', textAlign: 'center' }; + return ( +
+ +
start
+
start
+ + +
center
+
center
+ + +
end
+
end
+ + +
between
+
between
+ + + ); +}`, }, - }, + ], }, -} as Meta; - -type Story = StoryObj; +}; +// Keep original for backwards compatibility export const GridStory: Story = { render: ({ align, justify, wrap, ...rest }: RowProps & ColProps) => { const [gutter, setGutter] = useState(24); diff --git a/superset-frontend/packages/superset-ui-core/src/components/IconButton/IconButton.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/IconButton/IconButton.stories.tsx index acf1085b3e3d..d667a28c9291 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/IconButton/IconButton.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/IconButton/IconButton.stories.tsx @@ -22,6 +22,28 @@ import { IconButton } from '.'; export default { title: 'Components/IconButton', component: IconButton, + parameters: { + docs: { + description: { + component: + 'The IconButton component is a versatile button that allows you to combine an icon with a text label. It is designed for use in situations where you want to display an icon along with some text in a single clickable element.', + }, + a11y: { + enabled: true, + }, + }, + }, +} as Meta; + +type Story = StoryObj; + +export const InteractiveIconButton: Story = { + args: { + buttonText: 'IconButton', + altText: 'Icon button alt text', + padded: true, + icon: 'https://superset.apache.org/img/superset-logo-horiz.svg', + }, argTypes: { altText: { control: 'text', @@ -33,22 +55,21 @@ export default { }, buttonText: { control: 'text', - description: 'The text inside the button', + description: 'The text inside the button.', table: { type: { summary: 'string' }, }, }, icon: { - control: false, - description: 'Icon inside the button', + control: 'text', + description: 'Icon inside the button (URL or path).', table: { type: { summary: 'string' }, - defaultValue: { summary: 'string' }, }, }, padded: { control: 'boolean', - description: 'add padding between icon and button text', + description: 'Add padding between icon and button text.', table: { type: { summary: 'boolean' }, }, @@ -57,17 +78,21 @@ export default { parameters: { docs: { description: { - component: - 'The IconButton component is a versatile button that allows you to combine an icon with a text label. It is designed for use in situations where you want to display an icon along with some text in a single clickable element.', - }, - a11y: { - enabled: true, + story: 'A button with an icon and text label.', }, + liveExample: `function Demo() { + return ( + alert('Clicked!')} + /> + ); +}`, }, }, -} as Meta; - -type Story = StoryObj; +}; export const Default: Story = { args: { @@ -78,6 +103,6 @@ export const Default: Story = { export const CustomIcon: Story = { args: { buttonText: 'Custom icon IconButton', - icon: '/images/sqlite.png', + icon: 'https://superset.apache.org/img/superset-logo-horiz.svg', }, }; diff --git a/superset-frontend/packages/superset-ui-core/src/components/IconTooltip/IconTooltip.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/IconTooltip/IconTooltip.stories.tsx index d254c8c4cc20..f6bf8361b0fe 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/IconTooltip/IconTooltip.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/IconTooltip/IconTooltip.stories.tsx @@ -64,5 +64,29 @@ InteractiveIconTooltip.argTypes = { defaultValue: 'top', control: { type: 'select' }, options: PLACEMENTS, + description: 'Position of the tooltip relative to the icon.', + }, + tooltip: { + control: { type: 'text' }, + description: 'Text content to display in the tooltip.', + }, +}; + +InteractiveIconTooltip.parameters = { + docs: { + description: { + story: + 'A tooltip wrapper for icons. Pass an icon component as children and specify tooltip text.', + }, + sampleChildren: [ + { component: 'Icons.InfoCircleOutlined', props: { iconSize: 'l' } }, + ], + liveExample: `function Demo() { + return ( + + + + ); +}`, }, }; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Icons/Icons.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Icons/Icons.stories.tsx index 061c2609c633..8aeea5fd2b82 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Icons/Icons.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Icons/Icons.stories.tsx @@ -26,6 +26,14 @@ import { BaseIconComponent } from './BaseIcon'; export default { title: 'Components/Icons', component: BaseIconComponent, + parameters: { + docs: { + description: { + component: + 'Icon library for Apache Superset. Contains over 200 icons based on Ant Design icons with consistent sizing and theming support.', + }, + }, + }, }; const palette: Record = { @@ -102,6 +110,10 @@ export const InteractiveIcons = ({ ); }; +InteractiveIcons.args = { + iconSize: 'xl', +}; + InteractiveIcons.argTypes = { showNames: { name: 'Show names', @@ -112,6 +124,8 @@ InteractiveIcons.argTypes = { defaultValue: 'xl', control: { type: 'inline-radio' }, options: ['s', 'm', 'l', 'xl', 'xxl'], + description: + 'Size of the icons: s (12px), m (16px), l (20px), xl (24px), xxl (32px).', }, iconColor: { defaultValue: null, @@ -124,3 +138,168 @@ InteractiveIcons.argTypes = { }, }, }; + +InteractiveIcons.parameters = { + docs: { + // Use a specific icon for the live example since Icons is a namespace, not a component + renderComponent: 'Icons.InfoCircleOutlined', + liveExample: `function Demo() { + return ( +
+ + + + +
+ ); +}`, + examples: [ + { + title: 'Icon Sizes', + code: `function IconSizes() { + const sizes = ['s', 'm', 'l', 'xl', 'xxl']; + return ( +
+ {sizes.map(size => ( +
+ +
{size}
+
+ ))} +
+ ); +}`, + }, + { + title: 'Icon Gallery', + code: `function IconGallery() { + const Section = ({ title, children }) => ( +
+
{title}
+
{children}
+
+ ); + return ( +
+
+ + + + + + +
+
+ + + + + + + + +
+
+ + + + + + + + + + + + + + +
+
+ + + + + + + + + + +
+
+ + + + + + + + + +
+
+ + + + + + + + + + + + + + + +
+
+ + + + +
+
+ + + + + + + + +
+
+ ); +}`, + }, + { + title: 'Icon with Text', + code: `function IconWithText() { + return ( +
+
+ + Success message +
+
+ + Information message +
+
+ + Warning message +
+
+ + Error message +
+
+ ); +}`, + }, + ], + }, +}; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Input/Input.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Input/Input.stories.tsx index 0d0ce36eb92d..e452dc90e9d4 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Input/Input.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Input/Input.stories.tsx @@ -16,17 +16,15 @@ * specific language governing permissions and limitations * under the License. */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { StoryObj } from '@storybook/react'; import { Input, InputNumber } from '.'; import type { InputProps, InputNumberProps, TextAreaProps } from './types'; -const meta: Meta = { +export default { title: 'Components/Input', component: Input, }; -export default meta; - type Story = StoryObj; type InputNumberStory = StoryObj; type TextAreaStory = StoryObj; @@ -41,6 +39,16 @@ export const InteractiveInput: Story = { variant: 'outlined', }, argTypes: { + type: { + control: { type: 'select' }, + options: ['text', 'password', 'email', 'number', 'tel', 'url', 'search'], + description: 'HTML input type', + table: { + category: 'Input', + type: { summary: 'string' }, + defaultValue: { summary: 'text' }, + }, + }, defaultValue: { control: { type: 'text' }, description: 'Default input value', diff --git a/superset-frontend/packages/superset-ui-core/src/components/Label/Label.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Label/Label.stories.tsx index a1012c76e77e..b14e8833180d 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Label/Label.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Label/Label.stories.tsx @@ -95,3 +95,36 @@ LabelGallery.argTypes = { control: { type: 'boolean' }, }, }; + +// Interactive single Label story +interface InteractiveLabelProps { + type: LabelType; + children: string; + monospace?: boolean; +} + +export const InteractiveLabel: StoryFn = args => ( + +); + +InteractiveLabel.args = { + type: 'default', + children: 'Label text', + monospace: false, +}; + +InteractiveLabel.argTypes = { + type: { + description: 'The visual style of the label.', + options, + control: { type: 'select' }, + }, + children: { + description: 'The label text content.', + control: { type: 'text' }, + }, + monospace: { + description: 'Use monospace font.', + control: { type: 'boolean' }, + }, +}; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Layout/Layout.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Layout/Layout.stories.tsx index 9040d7b43fdd..16f766b23489 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Layout/Layout.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Layout/Layout.stories.tsx @@ -28,100 +28,190 @@ export default { title: 'Design System/Components/Layout', component: Layout, subcomponents: { Header, Footer, Sider, Content }, - argTypes: { - // Layout properties - className: { - control: false, - table: { - category: 'Layout', - type: { summary: 'string' }, - defaultValue: { summary: 'undefined' }, + parameters: { + docs: { + description: { + component: + 'Ant Design Layout component with configurable Sider, Header, Footer, and Content.', }, }, + }, +} as Meta; + +type Story = StoryObj; + +export const InteractiveLayout: Story = { + args: { + hasSider: false, + }, + argTypes: { hasSider: { control: 'boolean', - description: 'Include a sider', - table: { - category: 'Layout', - type: { summary: 'boolean' }, - }, - }, - // Layout.Sider properties - breakpoint: { - control: 'select', - options: ['xs', 'sm', 'md', 'lg', 'xl', 'xxl'], - description: 'Responsive breakpoint for the Sider', - table: { - category: 'Sider', - type: { summary: 'text' }, - }, + description: 'Whether the layout contains a Sider sub-component.', }, - collapsible: { - control: 'boolean', - description: 'Whether the Sider can be collapsed', - table: { - category: 'Sider', - type: { summary: 'boolean' }, - }, - }, - collapsed: { - control: 'boolean', - description: 'To set the current status of the Sider', - table: { - category: 'Sider', - type: { summary: 'boolean' }, - }, + }, + render: ({ + className, + hasSider, + ...siderProps + }: LayoutProps & SiderProps) => ( + + {hasSider && ( + +
+ + }> + Option 1 + + }> + Option 2 + + + + )} + +
+ Header +
+ + Content Area + +
+ Ant Design Layout Footer +
+
+ + ), +}; + +InteractiveLayout.parameters = { + docs: { + staticProps: { + style: { minHeight: 200 }, }, - collapsedWith: { - control: false, - description: - 'Width of the collapsed sidebar, by setting to 0 a special trigger will appear', - table: { - category: 'Sider', - type: { summary: 'number' }, - defaultValue: 80, + sampleChildren: [ + { + component: 'Layout.Header', + props: { + children: 'Header', + style: { + background: '#001529', + color: '#fff', + padding: '0 24px', + lineHeight: '64px', + }, + }, }, - }, - reverseArrow: { - control: 'boolean', - description: 'Whether the arrow icon is reversed', - table: { - category: 'Sider', - type: { summary: 'boolean' }, + { + component: 'Layout.Content', + props: { + children: 'Content Area', + style: { padding: '24px', background: '#fff', flex: 1 }, + }, }, - }, - theme: { - control: 'select', - options: ['light', 'dark'], - description: 'Theme for the Sider', - table: { - category: 'Sider', - type: { summary: 'string' }, - defaultValue: { summary: 'dark' }, + { + component: 'Layout.Footer', + props: { + children: 'Footer', + style: { + textAlign: 'center', + background: '#f5f5f5', + padding: '12px', + }, + }, }, + ], + description: { + story: 'Layout component with Header, Footer, Sider, and Content areas.', }, - width: { - control: 'number', - description: 'Width of the Sider', - table: { - category: 'Sider', - type: { summary: 'number' }, - defaultValue: { summary: '200' }, + liveExample: `function Demo() { + return ( + + +
Sidebar
+
+ + + Header + + + Content + + + Footer + + +
+ ); +}`, + examples: [ + { + title: 'Content Only', + code: `function ContentOnly() { + return ( + + + Application Header + + + Main content area without a sidebar + + + Footer Content + + + ); +}`, }, - }, - }, - parameters: { - docs: { - description: { - component: - 'Ant Design Layout component with configurable Sider, Header, Footer, and Content.', + { + title: 'Right Sidebar', + code: `function RightSidebar() { + return ( + + + + Header + + + Content with right sidebar + + + +
Right Sidebar
+
+
+ ); +}`, }, - }, + ], }, -} as Meta; - -type Story = StoryObj; +}; +// Keep original for backwards compatibility export const LayoutStory: Story = { render: ({ className, diff --git a/superset-frontend/packages/superset-ui-core/src/components/List/List.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/List/List.stories.tsx index efb5bc093723..3a38db424fb5 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/List/List.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/List/List.stories.tsx @@ -38,7 +38,6 @@ export const InteractiveList = (args: ListProps) => ( InteractiveList.args = { bordered: false, split: true, - itemLayout: 'horizontal', size: 'default', loading: false, }; @@ -46,20 +45,42 @@ InteractiveList.args = { InteractiveList.argTypes = { bordered: { control: { type: 'boolean' }, + description: 'Whether to show a border around the list.', }, split: { control: { type: 'boolean' }, + description: 'Whether to show a divider between items.', }, loading: { control: { type: 'boolean' }, - }, - itemLayout: { - control: { type: 'select' }, - options: ['horizontal', 'vertical'], + description: 'Whether to show a loading indicator.', }, size: { control: { type: 'select' }, options: ['default', 'small', 'large'], + description: 'Size of the list.', + }, +}; + +InteractiveList.parameters = { + docs: { + description: { + story: + 'A list component for displaying rows of data. Requires dataSource array and renderItem function.', + }, + staticProps: { + dataSource: ['Dashboard Analytics', 'User Management', 'Data Sources'], + }, + liveExample: `function Demo() { + const data = ['Dashboard Analytics', 'User Management', 'Data Sources']; + return ( + {item}} + /> + ); +}`, }, }; diff --git a/superset-frontend/packages/superset-ui-core/src/components/ListViewCard/ListViewCard.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/ListViewCard/ListViewCard.stories.tsx index 70bf20c592a3..0b45f38bb358 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/ListViewCard/ListViewCard.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/ListViewCard/ListViewCard.stories.tsx @@ -16,29 +16,97 @@ * specific language governing permissions and limitations * under the License. */ +import type { Meta, StoryObj } from '@storybook/react'; import { ListViewCard } from '.'; export default { title: 'Components/ListViewCard', component: ListViewCard, + parameters: { + docs: { + description: { + component: + 'ListViewCard is a card component used to display items in list views with an image, title, description, and optional cover sections.', + }, + }, + }, +} as Meta; + +type Story = StoryObj; + +export const InteractiveListViewCard: Story = { + args: { + title: 'Superset Card Title', + loading: false, + url: '/superset/dashboard/births/', + imgURL: 'https://picsum.photos/seed/superset/300/200', + description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit...', + coverLeft: 'Left Section', + coverRight: 'Right Section', + }, argTypes: { - loading: { control: 'boolean' }, + title: { + control: { type: 'text' }, + description: 'Title displayed on the card.', + }, + loading: { + control: { type: 'boolean' }, + description: 'Whether the card is in loading state.', + }, + url: { + name: 'url', + control: { type: 'text' }, + description: 'URL the card links to.', + }, + imgURL: { + name: 'imgURL', + control: { type: 'text' }, + description: 'Primary image URL for the card.', + }, + description: { + control: { type: 'text' }, + description: 'Description text displayed on the card.', + }, + coverLeft: { + control: { type: 'text' }, + description: 'Content for the left section of the cover.', + }, + coverRight: { + control: { type: 'text' }, + description: 'Content for the right section of the cover.', + }, + }, + parameters: { + docs: { + description: { + story: + 'A card component for displaying items in list views with images and descriptions.', + }, + liveExample: `function Demo() { + return ( + + ); +}`, + }, }, }; -export const SupersetListViewCard = ({ - loading = false, -}: { - loading?: boolean; -}) => ( - -); +// Keep original for backwards compatibility +export const SupersetListViewCard: Story = { + args: { + title: 'Superset Card Title', + loading: false, + url: '/superset/dashboard/births/', + imgURL: 'https://picsum.photos/seed/superset2/300/200', + description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit...', + coverLeft: 'Left Section', + coverRight: 'Right Section', + }, +}; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Loading/Loading.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Loading/Loading.stories.tsx index 92300215bb39..5bcc96558d22 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Loading/Loading.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Loading/Loading.stories.tsx @@ -223,25 +223,120 @@ ContextualExamples.parameters = { export const InteractiveLoading = (args: LoadingProps) => ; InteractiveLoading.args = { - image: '', - className: '', size: 'm', + position: 'normal', muted: false, }; InteractiveLoading.argTypes = { position: { - name: 'position', control: { type: 'select' }, options: POSITIONS, + description: + 'Position style: normal (inline flow), floating (overlay), or inline.', }, size: { - name: 'size', control: { type: 'select' }, options: SIZES, + description: 'Size of the spinner: s (40px), m (70px), or l (100px).', }, muted: { - name: 'muted', control: { type: 'boolean' }, + description: 'Whether to show a muted/subtle version of the spinner.', + }, +}; + +InteractiveLoading.parameters = { + docs: { + description: { + story: 'A loading spinner component with configurable size and position.', + }, + liveExample: `function Demo() { + return ( +
+ {['normal', 'floating', 'inline'].map(position => ( +
+

{position}

+ +
+ ))} +
+ ); +}`, + examples: [ + { + title: 'Size and Opacity Showcase', + code: `function SizeShowcase() { + const sizes = ['s', 'm', 'l']; + return ( +
+
+
Size
+
Normal
+
Muted
+
Usage
+ {sizes.map(size => ( + +
+ {size.toUpperCase()} ({size === 's' ? '40px' : size === 'm' ? '70px' : '100px'}) +
+
+ +
+
+ +
+
+ {size === 's' && 'Filter bars, inline'} + {size === 'm' && 'Explore pages'} + {size === 'l' && 'Full page loading'} +
+
+ ))} +
+
+ ); +}`, + }, + { + title: 'Contextual Examples', + code: `function ContextualDemo() { + return ( +
+

Filter Bar (size="s", muted)

+
+ Filter 1: + + Filter 2: + +
+ +

Dashboard Grid (size="s", muted)

+
+ {[1, 2, 3].map(i => ( +
+ +
+ ))} +
+ +

Main Loading (size="l")

+
+ +
+
+ ); +}`, + }, + ], }, }; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Menu/Menu.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Menu/Menu.stories.tsx index b7bf895a2f4a..2789edf4dd7e 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Menu/Menu.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Menu/Menu.stories.tsx @@ -21,6 +21,14 @@ import { Menu, MainNav } from '.'; export default { title: 'Components/Menu', component: Menu as React.FC, + parameters: { + docs: { + description: { + component: + 'Navigation menu component supporting horizontal, vertical, and inline modes. Based on Ant Design Menu with Superset styling.', + }, + }, + }, }; export const MainNavigation = (args: any) => ( @@ -47,18 +55,97 @@ export const InteractiveMenu = (args: any) => ( ); InteractiveMenu.args = { - defaultSelectedKeys: ['1'], - inlineCollapsed: false, mode: 'horizontal', - multiple: false, selectable: true, }; InteractiveMenu.argTypes = { mode: { - control: { - type: 'select', - }, + control: 'select', options: ['horizontal', 'vertical', 'inline'], + description: + 'Menu display mode: horizontal navbar, vertical sidebar, or inline collapsible.', + }, + selectable: { + control: 'boolean', + description: 'Whether menu items can be selected.', + }, + multiple: { + control: 'boolean', + description: 'Allow multiple items to be selected.', + }, + inlineCollapsed: { + control: 'boolean', + description: + 'Whether the inline menu is collapsed (only applies to inline mode).', + }, +}; + +InteractiveMenu.parameters = { + docs: { + staticProps: { + items: [ + { label: 'Dashboards', key: 'dashboards' }, + { label: 'Charts', key: 'charts' }, + { label: 'Datasets', key: 'datasets' }, + { label: 'SQL Lab', key: 'sqllab' }, + ], + }, + liveExample: `function Demo() { + return ( + + ); +}`, + examples: [ + { + title: 'Vertical Menu', + code: `function VerticalMenu() { + return ( + + ); +}`, + }, + { + title: 'Menu with Icons', + code: `function MenuWithIcons() { + return ( + Dashboards, key: 'dashboards' }, + { label: <> Charts, key: 'charts' }, + { label: <> Datasets, key: 'datasets' }, + { label: <> SQL Lab, key: 'sqllab' }, + ]} + /> + ); +}`, + }, + ], }, }; diff --git a/superset-frontend/packages/superset-ui-core/src/components/MetadataBar/MetadataBar.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/MetadataBar/MetadataBar.stories.tsx index 8f7d0d700dcc..9228769255e4 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/MetadataBar/MetadataBar.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/MetadataBar/MetadataBar.stories.tsx @@ -21,8 +21,16 @@ import { useResizeDetector } from 'react-resize-detector'; import MetadataBar, { MetadataBarProps, MetadataType } from '.'; export default { - title: 'Design System/Components/MetadataBar/Examples', + title: 'Design System/Components/MetadataBar', component: MetadataBar, + parameters: { + docs: { + description: { + component: + 'MetadataBar displays a row of metadata items (SQL info, owners, last modified, tags, dashboards, etc.) that collapse responsively based on available width.', + }, + }, + }, }; const A_WEEK_AGO = 'a week ago'; @@ -98,3 +106,112 @@ Basic.argTypes = { }, }, }; + +// Interactive story for docs generation +export const InteractiveMetadataBar = (args: MetadataBarProps) => ( + +); + +InteractiveMetadataBar.args = { + items: [ + { + type: MetadataType.Sql, + title: 'Click to view query', + }, + { + type: MetadataType.Owner, + createdBy: 'Jane Smith', + owners: ['John Doe', 'Mary Wilson'], + createdOn: A_WEEK_AGO, + }, + { + type: MetadataType.LastModified, + value: A_WEEK_AGO, + modifiedBy: 'Jane Smith', + }, + { + type: MetadataType.Tags, + values: ['management', 'research', 'poc'], + }, + { + type: MetadataType.Dashboards, + title: 'Added to 3 dashboards', + description: 'To preview the list of dashboards go to More settings.', + }, + ], +}; + +InteractiveMetadataBar.argTypes = {}; + +InteractiveMetadataBar.parameters = { + docs: { + staticProps: { + items: [ + { type: 'sql', title: 'Click to view query' }, + { + type: 'owner', + createdBy: 'Jane Smith', + owners: ['John Doe', 'Mary Wilson'], + createdOn: 'a week ago', + }, + { + type: 'lastModified', + value: 'a week ago', + modifiedBy: 'Jane Smith', + }, + { type: 'tags', values: ['management', 'research', 'poc'] }, + { + type: 'dashboards', + title: 'Added to 3 dashboards', + description: 'To preview the list of dashboards go to More settings.', + }, + ], + }, + liveExample: `function Demo() { + const items = [ + { type: 'sql', title: 'Click to view query' }, + { + type: 'owner', + createdBy: 'Jane Smith', + owners: ['John Doe', 'Mary Wilson'], + createdOn: 'a week ago', + }, + { + type: 'lastModified', + value: 'a week ago', + modifiedBy: 'Jane Smith', + }, + { type: 'tags', values: ['management', 'research', 'poc'] }, + ]; + return ; +}`, + examples: [ + { + title: 'Minimal Metadata', + code: `function MinimalMetadata() { + const items = [ + { type: 'owner', createdBy: 'Admin', owners: ['Admin'], createdOn: 'yesterday' }, + { type: 'lastModified', value: '2 hours ago', modifiedBy: 'Admin' }, + ]; + return ; +}`, + }, + { + title: 'Full Metadata', + code: `function FullMetadata() { + const items = [ + { type: 'sql', title: 'SELECT * FROM ...' }, + { type: 'owner', createdBy: 'Jane Smith', owners: ['Jane Smith', 'John Doe', 'Bob Wilson'], createdOn: '2 weeks ago' }, + { type: 'lastModified', value: '3 days ago', modifiedBy: 'John Doe' }, + { type: 'tags', values: ['production', 'finance', 'quarterly'] }, + { type: 'dashboards', title: 'Used in 12 dashboards' }, + { type: 'description', value: 'This chart shows quarterly revenue breakdown by region and product line.' }, + { type: 'rows', title: '1.2M rows' }, + { type: 'table', title: 'public.revenue_data' }, + ]; + return ; +}`, + }, + ], + }, +}; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Modal/Modal.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Modal/Modal.stories.tsx index 3b32618b495c..66547cbe3f70 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Modal/Modal.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Modal/Modal.stories.tsx @@ -24,6 +24,14 @@ import type { ModalProps, ModalFuncProps } from './types'; export default { title: 'Components/Modal', component: Modal, + parameters: { + docs: { + description: { + component: + 'Modal dialog component for displaying content that requires user attention or interaction. Supports customizable buttons, drag/resize, and confirmation dialogs.', + }, + }, + }, }; export const InteractiveModal = (props: ModalProps) => ( @@ -32,9 +40,9 @@ export const InteractiveModal = (props: ModalProps) => ( InteractiveModal.args = { disablePrimaryButton: false, - primaryButtonName: 'Danger', - primaryButtonStyle: 'danger', - show: true, + primaryButtonName: 'Submit', + primaryButtonStyle: 'primary', + show: false, title: "I'm a modal!", resizable: false, draggable: false, @@ -42,10 +50,119 @@ InteractiveModal.args = { }; InteractiveModal.argTypes = { + show: { + control: 'boolean', + description: + 'Whether the modal is visible. Use the "Try It" example below for a working demo.', + }, + title: { + control: 'text', + description: 'Title displayed in the modal header.', + }, + primaryButtonName: { + control: 'text', + description: 'Text for the primary action button.', + }, + primaryButtonStyle: { + control: 'select', + options: ['primary', 'secondary', 'dashed', 'danger', 'link'], + description: 'The style of the primary action button.', + }, + width: { + control: 'number', + description: 'Width of the modal in pixels.', + }, + resizable: { + control: 'boolean', + description: 'Whether the modal can be resized by dragging corners.', + }, + draggable: { + control: 'boolean', + description: 'Whether the modal can be dragged by its header.', + }, + disablePrimaryButton: { + control: 'boolean', + description: 'Whether the primary button is disabled.', + }, onHandledPrimaryAction: { action: 'onHandledPrimaryAction' }, onHide: { action: 'onHide' }, }; +InteractiveModal.parameters = { + docs: { + triggerProp: 'show', + onHideProp: 'onHide', + liveExample: `function ModalDemo() { + const [isOpen, setIsOpen] = React.useState(false); + return ( + <> + + setIsOpen(false)} + title="Example Modal" + primaryButtonName="Submit" + onHandledPrimaryAction={() => { + alert('Submitted!'); + setIsOpen(false); + }} + > +

This is the modal content. Click Submit or close the modal.

+
+ + ); +}`, + examples: [ + { + title: 'Danger Modal', + code: `function DangerModal() { + const [isOpen, setIsOpen] = React.useState(false); + return ( + <> + + setIsOpen(false)} + title="Confirm Delete" + primaryButtonName="Delete" + primaryButtonStyle="danger" + onHandledPrimaryAction={() => { + alert('Deleted!'); + setIsOpen(false); + }} + > +

Are you sure you want to delete this item? This action cannot be undone.

+
+ + ); +}`, + }, + { + title: 'Confirmation Dialogs', + code: `function ConfirmationDialogs() { + return ( +
+ + + +
+ ); +}`, + }, + ], + }, +}; + export const ModalFunctions = (props: ModalFuncProps) => (
diff --git a/superset-frontend/packages/superset-ui-core/src/components/ModalTrigger/ModalTrigger.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/ModalTrigger/ModalTrigger.stories.tsx index 739cfecc87a2..3fd8bad4bfd4 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/ModalTrigger/ModalTrigger.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/ModalTrigger/ModalTrigger.stories.tsx @@ -39,6 +39,14 @@ interface IModalTriggerProps { export default { title: 'Components/ModalTrigger', component: ModalTrigger, + parameters: { + docs: { + description: { + component: + 'A component that renders a trigger element which opens a modal when clicked. Useful for actions that need confirmation or additional input.', + }, + }, + }, }; export const InteractiveModalTrigger = (args: IModalTriggerProps) => ( @@ -47,13 +55,116 @@ export const InteractiveModalTrigger = (args: IModalTriggerProps) => ( InteractiveModalTrigger.args = { isButton: true, - modalTitle: 'I am a modal title', - modalBody: 'I am a modal body', - modalFooter: 'I am a modal footer', - tooltip: 'I am a tooltip', + modalTitle: 'Modal Title', + modalBody: 'This is the modal body content.', + tooltip: 'Click to open modal', width: '600px', maxWidth: '1000px', responsive: true, draggable: false, resizable: false, }; + +InteractiveModalTrigger.argTypes = { + triggerNode: { + control: false, + description: 'The clickable element that opens the modal when clicked.', + }, + isButton: { + control: 'boolean', + description: 'Whether to wrap the trigger in a button element.', + }, + modalTitle: { + control: 'text', + description: 'Title displayed in the modal header.', + }, + modalBody: { + control: 'text', + description: 'Content displayed in the modal body.', + }, + tooltip: { + control: 'text', + description: 'Tooltip text shown on hover over the trigger.', + }, + width: { + control: 'text', + description: 'Width of the modal (e.g., "600px", "80%").', + }, + maxWidth: { + control: 'text', + description: 'Maximum width of the modal.', + }, + responsive: { + control: 'boolean', + description: 'Whether the modal should be responsive.', + }, + draggable: { + control: 'boolean', + description: 'Whether the modal can be dragged by its header.', + }, + resizable: { + control: 'boolean', + description: 'Whether the modal can be resized by dragging corners.', + }, +}; + +InteractiveModalTrigger.parameters = { + docs: { + // Use a simple span for triggerNode since isButton: true wraps it in a button + staticProps: { + triggerNode: 'Click to Open Modal', + }, + liveExample: `function Demo() { + return ( + Click to Open} + modalTitle="Example Modal" + modalBody={

This is the modal content. You can put any React elements here.

} + width="500px" + responsive + /> + ); +}`, + examples: [ + { + title: 'With Custom Trigger', + code: `function CustomTrigger() { + return ( + + Add New Item + + } + modalTitle="Add New Item" + modalBody={ +
+

Fill out the form to add a new item.

+ +
+ } + width="400px" + /> + ); +}`, + }, + { + title: 'Draggable & Resizable', + code: `function DraggableModal() { + return ( + Open Draggable Modal} + modalTitle="Draggable & Resizable" + modalBody={

Try dragging the header or resizing from the corners!

} + draggable + resizable + width="500px" + /> + ); +}`, + }, + ], + }, +}; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Popover/Popover.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Popover/Popover.stories.tsx index c7d58485ece3..f1c3c9b8faad 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Popover/Popover.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Popover/Popover.stories.tsx @@ -22,6 +22,14 @@ import { Button } from '../Button'; export default { title: 'Components/Popover', component: Popover, + parameters: { + docs: { + description: { + component: + 'A floating card that appears when hovering or clicking a trigger element. Supports configurable placement, trigger behavior, and custom content.', + }, + }, + }, }; export const InteractivePopover = (args: PopoverProps) => ( @@ -37,31 +45,6 @@ export const InteractivePopover = (args: PopoverProps) => ( ); -const PLACEMENTS = { - label: 'placement', - options: [ - 'topLeft', - 'top', - 'topRight', - 'leftTop', - 'left', - 'leftBottom', - 'rightTop', - 'right', - 'rightBottom', - 'bottomLeft', - 'bottom', - 'bottomRight', - ], - defaultValue: null, -}; - -const TRIGGERS = { - label: 'trigger', - options: ['hover', 'click', 'focus'], - defaultValue: null, -}; - InteractivePopover.args = { content: 'Popover sample content', title: 'Popover title', @@ -70,24 +53,116 @@ InteractivePopover.args = { }; InteractivePopover.argTypes = { + content: { + control: 'text', + description: 'Content displayed inside the popover body.', + }, + title: { + control: 'text', + description: 'Title displayed in the popover header.', + }, placement: { - name: PLACEMENTS.label, control: { type: 'select' }, - options: PLACEMENTS.options, + options: [ + 'topLeft', + 'top', + 'topRight', + 'leftTop', + 'left', + 'leftBottom', + 'rightTop', + 'right', + 'rightBottom', + 'bottomLeft', + 'bottom', + 'bottomRight', + ], + description: 'Position of the popover relative to the trigger element.', }, trigger: { - name: TRIGGERS.label, control: { type: 'select' }, - options: TRIGGERS.options, + options: ['hover', 'click', 'focus'], + description: 'Event that triggers the popover to appear.', }, arrow: { - name: 'arrow', control: { type: 'boolean' }, - description: "Change arrow's visible state", + description: "Whether to show the popover's arrow pointing to the trigger.", }, color: { - name: 'color', control: { type: 'color' }, description: 'The background color of the popover.', }, }; + +InteractivePopover.parameters = { + docs: { + sampleChildren: [{ component: 'Button', props: { children: 'Hover me' } }], + liveExample: `function Demo() { + return ( + + + + ); +}`, + examples: [ + { + title: 'Click Trigger', + code: `function ClickPopover() { + return ( + + + + ); +}`, + }, + { + title: 'Placements', + code: `function PlacementsDemo() { + return ( +
+ {['top', 'right', 'bottom', 'left'].map(placement => ( + + + + ))} +
+ ); +}`, + }, + { + title: 'Rich Content', + code: `function RichPopover() { + return ( + +

Created by: Admin

+

Last modified: Jan 2025

+

Charts: 12

+
+ } + > + + + ); +}`, + }, + ], + }, +}; diff --git a/superset-frontend/packages/superset-ui-core/src/components/ProgressBar/ProgressBar.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/ProgressBar/ProgressBar.stories.tsx index 46182af6e17c..d056a992ad4d 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/ProgressBar/ProgressBar.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/ProgressBar/ProgressBar.stories.tsx @@ -21,10 +21,18 @@ import ProgressBar, { ProgressBarProps } from '.'; export default { title: 'Components/ProgressBar', component: ProgressBar, + parameters: { + docs: { + description: { + component: + 'Progress bar component for displaying completion status. Supports line, circle, and dashboard display types.', + }, + }, + }, }; export const InteractiveProgressBar = (args: ProgressBarProps) => ( - + ); export const InteractiveProgressCircle = (args: ProgressBarProps) => ( @@ -35,6 +43,120 @@ export const InteractiveProgressDashboard = (args: ProgressBarProps) => ( ); +InteractiveProgressBar.args = { + percent: 75, + status: 'normal', + type: 'line', + striped: false, + showInfo: true, + strokeLinecap: 'round', +}; + +InteractiveProgressBar.argTypes = { + percent: { + control: { type: 'number', min: 0, max: 100 }, + description: 'Completion percentage (0-100).', + }, + status: { + control: 'select', + options: ['normal', 'success', 'exception', 'active'], + description: 'Current status of the progress bar.', + }, + type: { + control: 'select', + options: ['line', 'circle', 'dashboard'], + description: 'Display type: line, circle, or dashboard gauge.', + }, + striped: { + control: 'boolean', + description: 'Whether to show striped animation on the bar.', + }, + showInfo: { + control: 'boolean', + description: 'Whether to show the percentage text.', + }, + strokeColor: { + control: 'color', + description: 'Color of the progress bar fill.', + }, + trailColor: { + control: 'color', + description: 'Color of the unfilled portion.', + }, + strokeLinecap: { + control: 'select', + options: ['round', 'butt', 'square'], + description: 'Shape of the progress bar endpoints.', + }, +}; + +InteractiveProgressBar.parameters = { + docs: { + liveExample: `function Demo() { + return ( + + ); +}`, + examples: [ + { + title: 'All Progress Types', + code: `function AllTypesDemo() { + return ( +
+
+

Line

+ +
+
+

Circle

+ +
+
+

Dashboard

+ +
+
+ ); +}`, + }, + { + title: 'Status Variants', + code: `function StatusDemo() { + const statuses = ['normal', 'success', 'exception', 'active']; + return ( +
+ {statuses.map(status => ( +
+ {status} + +
+ ))} +
+ ); +}`, + }, + { + title: 'Custom Colors', + code: `function CustomColors() { + return ( +
+ + + + +
+ ); +}`, + }, + ], + }, +}; + const commonArgs = { striped: true, percent: 90, @@ -46,39 +168,40 @@ const commonArgs = { }; const commonArgTypes = { + percent: { + control: { type: 'number', min: 0, max: 100 }, + description: 'Completion percentage (0-100).', + }, + striped: { + control: 'boolean', + description: 'Whether to show striped animation on the bar.', + }, + showInfo: { + control: 'boolean', + description: 'Whether to show the percentage text.', + }, + strokeColor: { + control: 'color', + description: 'Color of the progress bar.', + }, + trailColor: { + control: 'color', + description: 'Color of the unfilled portion.', + }, strokeLinecap: { - control: { - type: 'select', - }, + control: 'select', options: ['round', 'butt', 'square'], + description: 'Shape of the progress bar endpoints.', }, type: { - control: { - type: 'select', - }, + control: 'select', options: ['line', 'circle', 'dashboard'], - }, -}; - -InteractiveProgressBar.args = { - ...commonArgs, - status: 'normal', -}; - -InteractiveProgressBar.argTypes = { - ...commonArgTypes, - status: { - control: { - type: 'select', - }, - options: ['normal', 'success', 'exception', 'active'], + description: 'Display type: line, circle, or dashboard gauge.', }, }; InteractiveProgressCircle.args = commonArgs; - InteractiveProgressCircle.argTypes = commonArgTypes; InteractiveProgressDashboard.args = commonArgs; - InteractiveProgressDashboard.argTypes = commonArgTypes; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Radio/Radio.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Radio/Radio.stories.tsx index c0453ed038fa..403fd0eed731 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Radio/Radio.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Radio/Radio.stories.tsx @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import type { StoryObj } from '@storybook/react'; import { css } from '@apache-superset/core/ui'; import { Icons } from '@superset-ui/core/components/Icons'; import { Space } from '../Space'; @@ -25,20 +26,32 @@ export default { title: 'Components/Radio', component: Radio, tags: ['autodocs'], + parameters: { + docs: { + description: { + component: + 'Radio button component for selecting one option from a set. Supports standalone radio buttons, radio buttons styled as buttons, and grouped radio buttons with layout configuration.', + }, + }, + }, }; const RadioArgsType = { value: { control: 'text', - description: 'The value of the radio button.', + description: 'The value associated with this radio button.', }, disabled: { control: 'boolean', - description: 'Whether the radio button is disabled or not.', + description: 'Whether the radio button is disabled.', }, checked: { control: 'boolean', - description: 'The checked state of the radio button.', + description: 'Whether the radio button is checked (controlled mode).', + }, + children: { + control: 'text', + description: 'Label text displayed next to the radio button.', }, }; @@ -76,14 +89,66 @@ const radioGroupWrapperArgsType = { }, }; -export const RadioStory = { +export const RadioStory: StoryObj = { args: { value: 'radio1', disabled: false, checked: false, children: 'Radio', }, - argTypes: RadioArgsType, + argTypes: { + value: { + control: 'text', + description: 'The value associated with this radio button.', + }, + disabled: { + control: 'boolean', + description: 'Whether the radio button is disabled.', + }, + checked: { + control: 'boolean', + description: 'Whether the radio button is checked (controlled mode).', + }, + children: { + control: 'text', + description: 'Label text displayed next to the radio button.', + }, + }, +}; + +RadioStory.parameters = { + docs: { + examples: [ + { + title: 'Radio Button Variants', + code: `function RadioButtonDemo() { + const [value, setValue] = React.useState('line'); + return ( + setValue(e.target.value)}> + Line Chart + Bar Chart + Pie Chart + + ); +}`, + }, + { + title: 'Vertical Radio Group', + code: `function VerticalDemo() { + const [value, setValue] = React.useState('option1'); + return ( + setValue(e.target.value)}> +
+ First option + Second option + Third option +
+
+ ); +}`, + }, + ], + }, }; export const RadioButtonStory = (args: RadioProps) => ( diff --git a/superset-frontend/packages/superset-ui-core/src/components/Select/Select.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Select/Select.stories.tsx index 3323a487281e..97e7f840df4b 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Select/Select.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Select/Select.stories.tsx @@ -16,14 +16,20 @@ * specific language governing permissions and limitations * under the License. */ -import { StoryObj } from '@storybook/react'; -import { noop } from 'lodash'; import { SelectOptionsType, SelectProps } from './types'; import { Select } from '.'; export default { title: 'Components/Select', component: Select, + parameters: { + docs: { + description: { + component: + 'A versatile select component supporting single and multi-select modes, search filtering, option creation, and both synchronous and asynchronous data sources.', + }, + }, + }, }; const DEFAULT_WIDTH = 200; @@ -88,145 +94,210 @@ const generateOptions = (opts: SelectOptionsType, count: number) => { return generated.slice(0, count); }; -export const InteractiveSelect: StoryObj = { - render: ({ - header, - options, - optionsCount, - ...args - }: SelectProps & { header: string; optionsCount: number }) => { - noop(header); - return ( -
- +
+); + +InteractiveSelect.args = { + mode: 'single', + placeholder: 'Select ...', + showSearch: true, + allowNewOptions: false, + allowClear: false, + allowSelectAll: true, + disabled: false, + invertSelection: false, + oneLine: false, + maxTagCount: 4, +}; + +InteractiveSelect.argTypes = { + mode: { + control: 'inline-radio', + options: ['single', 'multiple'], + description: 'Whether to allow selection of a single option or multiple.', }, - args: { - autoFocus: true, - allowNewOptions: false, - allowClear: false, - autoClearSearchValue: false, - allowSelectAll: true, - disabled: false, - header: 'none', - invertSelection: false, - labelInValue: true, - maxTagCount: 4, - mode: 'single', - oneLine: false, - options, - optionsCount: options.length, - optionFilterProps: ['value', 'label', 'custom'], - placeholder: 'Select ...', - showSearch: true, + placeholder: { + control: 'text', + description: 'Placeholder text when no option is selected.', }, - argTypes: { - options: { - description: `It defines the options of the Select. - The options can be static, an array of options. - The options can also be async, a promise that returns an array of options. - `, - }, - ariaLabel: { - description: `It adds the aria-label tag for accessibility standards. - Must be plain English and localized. - `, - }, - labelInValue: { - table: { - disable: true, - }, - }, - name: { - table: { - disable: true, - }, + showSearch: { + control: 'boolean', + description: 'Whether to show a search input for filtering.', + }, + allowNewOptions: { + control: 'boolean', + description: + 'Whether users can create new options by typing a value not in the list.', + }, + allowClear: { + control: 'boolean', + description: 'Whether to show a clear button to reset the selection.', + }, + allowSelectAll: { + control: 'boolean', + description: 'Whether to show a "Select All" option in multiple mode.', + }, + disabled: { + control: 'boolean', + description: 'Whether the select is disabled.', + }, + invertSelection: { + control: 'boolean', + description: + 'Shows a stop icon instead of a checkmark on selected options, indicating deselection on click.', + }, + oneLine: { + control: 'boolean', + description: + 'Forces tags onto one line with overflow count. Requires multiple mode.', + }, + maxTagCount: { + control: { type: 'number' }, + description: + 'Maximum number of tags to display in multiple mode before showing an overflow count.', + }, +}; + +InteractiveSelect.parameters = { + docs: { + staticProps: { + options: [ + { + label: 'Such an incredibly awesome long long label', + value: 'long-label-1', + }, + { + label: 'Another incredibly awesome long long label', + value: 'long-label-2', + }, + { label: 'Option A', value: 'A' }, + { label: 'Option B', value: 'B' }, + { label: 'Option C', value: 'C' }, + { label: 'Option D', value: 'D' }, + { label: 'Option E', value: 'E' }, + { label: 'Option F', value: 'F' }, + { label: 'Option G', value: 'G' }, + { label: 'Option H', value: 'H' }, + { label: 'Option I', value: 'I' }, + ], }, - notFoundContent: { - table: { - disable: true, + liveExample: `function Demo() { + return ( +
+ +
+ ); +}`, }, - }, - mappedMode: { - table: { - disable: true, + { + title: 'Allow New Options', + code: `function AllowNewDemo() { + return ( +
+ +
+ ); +}`, }, - }, - allowNewOptions: { - description: `It enables the user to create new options. - Can be used with standard or async select types. - Can be used with any mode, single or multiple. False by default. - `, - }, - invertSelection: { - description: `It shows a stop-outlined icon at the far right of a selected - option instead of the default checkmark. - Useful to better indicate to the user that by clicking on a selected - option it will be de-selected. False by default. - `, - }, - optionFilterProps: { - description: `It allows to define which properties of the option object - should be looked for when searching. - By default label and value. - `, - }, - oneLine: { - description: `Sets maxTagCount to 1. The overflow tag is always displayed in - the same line, line wrapping is disabled. - When the dropdown is open, sets maxTagCount to 0, - displays only the overflow tag. - Requires '"mode=multiple"'. - `, - }, - maxTagCount: { - description: `Sets maxTagCount attribute. The overflow tag is displayed in - place of the remaining items. - Requires '"mode=multiple"'. - `, - }, - optionsCount: { - control: { - type: 'number', + { + title: 'One Line Mode', + code: `function OneLineDemo() { + return ( +
+ +
+ ); +}; + +AdvancedPlayground.args = { + autoFocus: true, + allowNewOptions: false, + allowClear: false, + autoClearSearchValue: false, + allowSelectAll: true, + disabled: false, + invertSelection: false, + labelInValue: true, + maxTagCount: 4, + mode: 'multiple', + oneLine: false, + optionsCount: options.length, + optionFilterProps: ['value', 'label', 'custom'], + placeholder: 'Select ...', + showSearch: true, +}; + +AdvancedPlayground.argTypes = { + mode: { + control: { type: 'inline-radio' }, + options: ['single', 'multiple'], + }, + optionsCount: { + control: { type: 'number' }, + }, +}; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Skeleton/Skeleton.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Skeleton/Skeleton.stories.tsx index cafbfdb1de7c..e873c30bbe8c 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Skeleton/Skeleton.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Skeleton/Skeleton.stories.tsx @@ -17,131 +17,114 @@ * under the License. */ import type { Meta, StoryObj } from '@storybook/react'; -import { type SkeletonButtonProps } from 'antd/es/skeleton/Button'; import { Space } from '../Space'; -import { AvatarProps } from '../Avatar/types'; import { Skeleton, type SkeletonProps } from '.'; const { Avatar, Button, Input, Image } = Skeleton; +type SkeletonStoryArgs = SkeletonProps & { + shape?: 'circle' | 'square'; + size?: 'large' | 'small' | 'default'; + block?: boolean; +}; + export default { title: 'Components/Skeleton', component: Skeleton, subcomponents: { Avatar, Button, Input, Image }, + parameters: { + docs: { + description: { + component: + 'Skeleton loading component with support for avatar, title, paragraph, button, and input placeholders.', + }, + }, + }, +} as Meta; + +type Story = StoryObj; + +export const InteractiveSkeleton: Story = { + args: { + active: true, + avatar: false, + loading: true, + title: true, + shape: 'circle', + size: 'default', + block: false, + }, argTypes: { - // Skeleton props active: { control: 'boolean', - description: 'Show animation effect', - table: { - category: 'Skeleton', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, + description: 'Show animation effect.', }, avatar: { control: 'boolean', - description: 'Show avatar placeholder', - table: { - category: 'Skeleton', - type: { summary: 'boolean | object' }, - defaultValue: { summary: false }, - }, + description: 'Show avatar placeholder.', }, loading: { control: 'boolean', - description: 'Display the skeleton when true', - table: { - category: 'Skeleton', - type: { summary: 'boolean' }, - }, - }, - paragraph: { - control: 'false', - description: 'Paragraph skeleton', - table: { - category: 'Skeleton', - type: { summary: 'boolean | object' }, - defaultValue: { summary: 'true' }, - }, - }, - round: { - control: false, - description: 'Show paragraph and title radius when true ', - table: { - category: 'Skeleton', - type: { summary: 'boolean' }, - defaultValue: { summary: false }, - }, + description: 'Display the skeleton when true.', }, title: { control: 'boolean', - description: 'Show title placeholder', - table: { - category: 'Skeleton', - type: { summary: 'boolean | object' }, - defaultValue: { summary: true }, - }, + description: 'Show title placeholder.', }, - - // Skeleton.Avatar props shape: { control: 'select', - description: 'Shape of the avatar', + description: 'Shape of the avatar/button skeleton.', options: ['circle', 'square'], - table: { - name: 'shape', - category: 'Avatar | Button', - type: { summary: 'string' }, - }, }, size: { control: 'select', options: ['large', 'small', 'default'], - description: 'Set the size of avatar in the skeleton', - table: { - category: 'Avatar | Button', - type: { summary: 'number | string' }, - }, - }, - - // Skeleton.Title props - width: { - control: false, - description: 'Set the width of title in the skeleton', - table: { - category: 'Title', - type: { summary: 'number | string' }, - }, + description: 'Size of the skeleton elements.', }, - - // Skeleton.Button props block: { control: 'boolean', - description: 'Option to fit button width to its parent width', - table: { - category: 'Button', - type: { summary: 'boolean' }, - defaultValue: { summary: false }, - }, + description: 'Option to fit button width to its parent width.', }, }, + render: args => { + const avatar = { + shape: args.shape, + size: args.size, + }; + const button = { + block: args.block, + shape: args.shape, + size: args.size, + }; + + return ( + + Skeleton + + Avatar + + Button + + + ); + }, parameters: { docs: { description: { - component: - 'Skeleton loading component with support for avatar, title, paragraph, button, and input placeholders.', + story: 'A loading placeholder for content that is not yet loaded.', }, + liveExample: `function Demo() { + return ( + + ); +}`, }, }, -} as Meta< - typeof Skeleton & typeof Avatar & typeof Button & typeof Input & typeof Image ->; - -type Story = StoryObj; +}; +// Keep original for backwards compatibility export const SkeletonStory: Story = { - render: (args: SkeletonProps & AvatarProps & SkeletonButtonProps) => { + render: args => { const avatar = { shape: args.shape, size: args.size, diff --git a/superset-frontend/packages/superset-ui-core/src/components/Slider/Slider.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Slider/Slider.stories.tsx index 121f1fa04845..3864bff4cd89 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Slider/Slider.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Slider/Slider.stories.tsx @@ -21,6 +21,14 @@ import Slider, { SliderSingleProps, SliderRangeProps } from '.'; export default { title: 'Components/Slider', component: Slider, + parameters: { + docs: { + description: { + component: + 'A slider input for selecting a value or range from a continuous or stepped interval. Supports single value, range, vertical orientation, marks, and tooltip display.', + }, + }, + }, }; const tooltipPlacement = [ @@ -75,33 +83,182 @@ InteractiveSlider.args = { max: 100, defaultValue: 70, step: 1, - marks: {}, disabled: false, reverse: false, vertical: false, - autoFocus: false, keyboard: true, dots: false, included: true, - tooltipPosition: 'bottom', }; InteractiveSlider.argTypes = { - onChange: { action: 'onChange' }, - onChangeComplete: { action: 'onChangeComplete' }, + min: { + control: { type: 'number' }, + description: 'Minimum value of the slider.', + }, + max: { + control: { type: 'number' }, + description: 'Maximum value of the slider.', + }, + defaultValue: { + control: { type: 'number' }, + description: 'Initial value of the slider.', + }, + step: { + control: { type: 'number' }, + description: 'Step increment between values. Use null for marks-only mode.', + }, + disabled: { + control: 'boolean', + description: 'Whether the slider is disabled.', + }, + reverse: { + control: 'boolean', + description: 'Whether to reverse the slider direction.', + }, + vertical: { + control: 'boolean', + description: 'Whether to display the slider vertically.', + }, + keyboard: { + control: 'boolean', + description: 'Whether keyboard arrow keys can control the slider.', + }, + dots: { + control: 'boolean', + description: 'Whether to show dots at each step mark.', + }, + included: { + control: 'boolean', + description: 'Whether to highlight the filled portion of the track.', + }, tooltipOpen: { - control: { type: 'boolean' }, + control: 'boolean', + description: 'Whether the value tooltip is always visible.', }, tooltipPosition: { - options: tooltipPlacement, control: { type: 'select' }, + options: [ + 'top', + 'left', + 'bottom', + 'right', + 'topLeft', + 'topRight', + 'bottomLeft', + 'bottomRight', + 'leftTop', + 'leftBottom', + 'rightTop', + 'rightBottom', + ], + description: 'Position of the value tooltip relative to the handle.', + }, + onChange: { action: 'onChange' }, + onChangeComplete: { action: 'onChangeComplete' }, +}; + +InteractiveSlider.parameters = { + docs: { + liveExample: `function Demo() { + return ( +
+ +
+ ); +}`, + examples: [ + { + title: 'Range Slider', + code: `function RangeSliderDemo() { + return ( +
+

Basic Range

+ +
+

Draggable Track

+ +
+ ); +}`, + }, + { + title: 'With Marks', + code: `function MarksDemo() { + return ( +
+ +
+ ); +}`, + }, + { + title: 'Stepped and Dots', + code: `function SteppedDemo() { + return ( +
+

Step = 10 with Dots

+ +
+

Step = 25

+ +
+ ); +}`, + }, + { + title: 'Vertical Slider', + code: `function VerticalDemo() { + return ( +
+ + + +
+ ); +}`, + }, + ], }, }; InteractiveRangeSlider.args = { - ...InteractiveSlider.args, + min: 0, + max: 100, defaultValue: [50, 70], + step: 1, + disabled: false, + reverse: false, + vertical: false, + keyboard: true, + dots: false, + included: true, draggableTrack: false, }; -InteractiveRangeSlider.argTypes = InteractiveSlider.argTypes; +InteractiveRangeSlider.argTypes = { + ...InteractiveSlider.argTypes, + draggableTrack: { + control: 'boolean', + description: + 'Whether the track between handles can be dragged to move both handles together.', + }, +}; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Space/Space.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Space/Space.stories.tsx index c6e935521d77..39ca191f0167 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Space/Space.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Space/Space.stories.tsx @@ -23,10 +23,23 @@ export default { component: Space, }; +// Sample children used in both Storybook and auto-generated docs +const SAMPLE_ITEMS = ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5']; + +// Shared styling for sample items - matches docs site rendering +const sampleItemStyle = { + padding: '8px 16px', + background: '#e6f4ff', + border: '1px solid #91caff', + borderRadius: '4px', +}; + export const InteractiveSpace = (args: SpaceProps) => ( - {new Array(20).fill(null).map((_, i) => ( -

Item

+ {SAMPLE_ITEMS.map((item, i) => ( +
+ {item} +
))}
); @@ -51,3 +64,75 @@ InteractiveSpace.argTypes = { options: ['small', 'middle', 'large'], }, }; + +InteractiveSpace.parameters = { + docs: { + // Inline for the static parser (can't resolve variable references) + sampleChildren: ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5'], + sampleChildrenStyle: { + padding: '8px 16px', + background: '#e6f4ff', + border: '1px solid #91caff', + borderRadius: '4px', + }, + liveExample: `function Demo() { + return ( + + {['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5'].map(item => ( +
+ {item} +
+ ))} +
+ ); +}`, + examples: [ + { + title: 'Vertical Space', + code: `function VerticalSpace() { + return ( + + + + + + ); +}`, + }, + { + title: 'Space Sizes', + code: `function SpaceSizes() { + const items = ['Item 1', 'Item 2', 'Item 3']; + const itemStyle = { + padding: '8px 16px', + background: '#e6f4ff', + border: '1px solid #91caff', + borderRadius: 4, + }; + return ( +
+ {['small', 'middle', 'large'].map(size => ( +
+

{size}

+ + {items.map(item => ( +
{item}
+ ))} +
+
+ ))} +
+ ); +}`, + }, + ], + }, +}; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Steps/Steps.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Steps/Steps.stories.tsx index 336c0ef0dd05..b61846b66e8b 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Steps/Steps.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Steps/Steps.stories.tsx @@ -22,12 +22,21 @@ import { Steps, type StepsProps } from '.'; export default { title: 'Components/Steps', component: Steps as typeof AntdSteps, + parameters: { + docs: { + description: { + component: + 'A navigation component for guiding users through multi-step workflows. Supports horizontal, vertical, and inline layouts with progress tracking.', + }, + }, + }, }; export const InteractiveSteps = (args: StepsProps) => ; + InteractiveSteps.args = { direction: 'horizontal', - initial: 0, + current: 1, labelPlacement: 'horizontal', progressDot: false, size: 'default', @@ -51,23 +60,145 @@ InteractiveSteps.args = { InteractiveSteps.argTypes = { direction: { - options: ['horizontal', 'vertical'], control: { type: 'select' }, + options: ['horizontal', 'vertical'], + description: 'Layout direction of the steps.', + }, + current: { + control: { type: 'number' }, + description: 'Index of the current step (zero-based).', }, labelPlacement: { - options: ['horizontal', 'vertical'], control: { type: 'select' }, + options: ['horizontal', 'vertical'], + description: 'Position of step labels relative to the step icon.', + }, + progressDot: { + control: 'boolean', + description: 'Whether to use a dot style instead of numbered icons.', }, size: { - options: ['default', 'small'], control: { type: 'select' }, + options: ['default', 'small'], + description: 'Size of the step icons and text.', }, status: { - options: ['wait', 'process', 'finish', 'error'], control: { type: 'select' }, + options: ['wait', 'process', 'finish', 'error'], + description: 'Status of the current step.', }, type: { - options: ['default', 'navigation', 'inline'], control: { type: 'select' }, + options: ['default', 'navigation', 'inline'], + description: + 'Visual style: default numbered, navigation breadcrumb, or inline compact.', + }, +}; + +InteractiveSteps.parameters = { + docs: { + staticProps: { + items: [ + { title: 'Connect Database', description: 'Configure the connection' }, + { title: 'Create Dataset', description: 'Select tables and columns' }, + { title: 'Build Chart', description: 'Choose visualization type' }, + ], + }, + liveExample: `function Demo() { + return ( + + ); +}`, + examples: [ + { + title: 'Vertical Steps', + code: `function VerticalSteps() { + return ( + + ); +}`, + }, + { + title: 'Status Indicators', + code: `function StatusSteps() { + return ( +
+
+

Error on Step 2

+ +
+
+

All Complete

+ +
+
+ ); +}`, + }, + { + title: 'Dot Style and Small Size', + code: `function DotAndSmall() { + return ( +
+
+

Progress Dots

+ +
+
+

Small Size

+ +
+
+ ); +}`, + }, + ], }, }; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Switch/Switch.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Switch/Switch.stories.tsx index 508549ac0e3f..19f3626a9a09 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Switch/Switch.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Switch/Switch.stories.tsx @@ -21,6 +21,14 @@ import { Switch, type SwitchProps } from '.'; export default { title: 'Components/Switch', + parameters: { + docs: { + description: { + component: + 'A toggle switch for boolean on/off states. Supports loading indicators, sizing, and an HTML title attribute for accessibility tooltips.', + }, + }, + }, }; export const InteractiveSwitch = ({ checked, ...rest }: SwitchProps) => { @@ -39,15 +47,121 @@ InteractiveSwitch.args = { checked: defaultCheckedValue, disabled: false, loading: false, - title: 'Switch', + title: 'Toggle feature', defaultChecked: defaultCheckedValue, - autoFocus: true, }; InteractiveSwitch.argTypes = { + checked: { + control: 'boolean', + description: 'Whether the switch is on.', + }, + disabled: { + control: 'boolean', + description: 'Whether the switch is disabled.', + }, + loading: { + control: 'boolean', + description: 'Whether to show a loading spinner inside the switch.', + }, + title: { + control: 'text', + description: + 'HTML title attribute shown as a browser tooltip on hover. Useful for accessibility.', + }, size: { - defaultValue: 'default', control: { type: 'radio' }, options: ['small', 'default'], + description: 'Size of the switch.', + }, +}; + +InteractiveSwitch.parameters = { + docs: { + liveExample: `function Demo() { + const [checked, setChecked] = React.useState(true); + return ( +
+ + {checked ? 'On' : 'Off'} + (hover the switch to see the title tooltip) +
+ ); +}`, + examples: [ + { + title: 'Switch States', + code: `function SwitchStates() { + return ( +
+
+ + Checked +
+
+ + Unchecked +
+
+ + Disabled (on) +
+
+ + Disabled (off) +
+
+ + Loading +
+
+ ); +}`, + }, + { + title: 'Sizes', + code: `function SizesDemo() { + return ( +
+
+ + Small +
+
+ + Default +
+
+ ); +}`, + }, + { + title: 'Settings Panel', + code: `function SettingsPanel() { + const [notifications, setNotifications] = React.useState(true); + const [darkMode, setDarkMode] = React.useState(false); + const [autoRefresh, setAutoRefresh] = React.useState(true); + return ( +
+

Dashboard Settings

+ {[ + { label: 'Email notifications', checked: notifications, onChange: setNotifications, title: 'Toggle email notifications' }, + { label: 'Dark mode', checked: darkMode, onChange: setDarkMode, title: 'Toggle dark mode' }, + { label: 'Auto-refresh data', checked: autoRefresh, onChange: setAutoRefresh, title: 'Toggle auto-refresh' }, + ].map(({ label, checked, onChange, title }) => ( +
+ {label} + +
+ ))} +
+ ); +}`, + }, + ], }, }; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Table/Table.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Table/Table.stories.tsx index 2a409e2a3c98..98ff8e0443bd 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Table/Table.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Table/Table.stories.tsx @@ -42,9 +42,16 @@ import HeaderWithRadioGroup from './header-renderers/HeaderWithRadioGroup'; import TimeCell from './cell-renderers/TimeCell'; export default { - title: 'Design System/Components/Table/Examples', + title: 'Design System/Components/Table', component: Table, - argTypes: { onClick: { action: 'clicked' } }, + parameters: { + docs: { + description: { + component: + 'A data table component with sorting, pagination, row selection, resizable columns, reorderable columns, and virtualization for large datasets.', + }, + }, + }, } as Meta; interface BasicData { @@ -211,6 +218,187 @@ const basicColumns: ColumnsType = [ }, ]; +// Interactive story for docs generation +export const InteractiveTable = (args: TableProps) => ( +
+); + +InteractiveTable.args = { + size: 'small', + bordered: false, + loading: false, + sticky: true, + resizable: false, + reorderable: false, + usePagination: false, + data: [ + { + key: 1, + name: 'Floppy Disk 10 pack', + category: 'Disk Storage', + price: 9.99, + }, + { key: 2, name: 'DVD 100 pack', category: 'Optical Storage', price: 27.99 }, + { key: 3, name: '128 GB SSD', category: 'Harddrive', price: 49.99 }, + { key: 4, name: '4GB 144mhz', category: 'Memory', price: 19.99 }, + { + key: 5, + name: '1GB USB Flash Drive', + category: 'Portable Storage', + price: 9.99, + }, + ], + columns: [ + { title: 'Name', dataIndex: 'name', key: 'name', width: 200 }, + { title: 'Category', dataIndex: 'category', key: 'category', width: 150 }, + { title: 'Price', dataIndex: 'price', key: 'price', width: 100 }, + ], +}; + +InteractiveTable.argTypes = { + size: { + control: 'select', + options: ['small', 'middle', 'large'], + description: 'Table size.', + }, + bordered: { + control: 'boolean', + description: 'Whether to show all table borders.', + }, + loading: { + control: 'boolean', + description: 'Whether the table is in a loading state.', + }, + sticky: { + control: 'boolean', + description: 'Whether the table header is sticky.', + }, + resizable: { + control: 'boolean', + description: 'Whether columns can be resized by dragging column edges.', + }, + reorderable: { + control: 'boolean', + description: + 'EXPERIMENTAL: Whether columns can be reordered by dragging. May not work in all contexts.', + }, + usePagination: { + control: 'boolean', + description: + 'Whether to enable pagination. When enabled, the table displays 5 rows per page.', + }, +}; + +InteractiveTable.parameters = { + docs: { + staticProps: { + height: 350, + defaultPageSize: 5, + pageSizeOptions: ['5', '10'], + data: [ + { + key: 1, + name: 'Floppy Disk 10 pack', + category: 'Disk Storage', + price: 9.99, + }, + { + key: 2, + name: 'DVD 100 pack', + category: 'Optical Storage', + price: 27.99, + }, + { key: 3, name: '128 GB SSD', category: 'Harddrive', price: 49.99 }, + { key: 4, name: '4GB 144mhz', category: 'Memory', price: 19.99 }, + { + key: 5, + name: '1GB USB Flash Drive', + category: 'Portable Storage', + price: 9.99, + }, + { key: 6, name: '256 GB SSD', category: 'Harddrive', price: 89.99 }, + { key: 7, name: '1 TB SSD', category: 'Harddrive', price: 349.99 }, + { key: 8, name: '16 GB DDR4', category: 'Memory', price: 59.99 }, + { key: 9, name: '32 GB DDR5', category: 'Memory', price: 129.99 }, + { + key: 10, + name: 'Blu-ray 50 pack', + category: 'Optical Storage', + price: 34.99, + }, + { + key: 11, + name: '64 GB USB Drive', + category: 'Portable Storage', + price: 14.99, + }, + { key: 12, name: '2 TB HDD', category: 'Harddrive', price: 59.99 }, + ], + columns: [ + { title: 'Name', dataIndex: 'name', key: 'name', width: 200 }, + { + title: 'Category', + dataIndex: 'category', + key: 'category', + width: 150, + }, + { title: 'Price', dataIndex: 'price', key: 'price', width: 100 }, + ], + }, + liveExample: `function Demo() { + const data = [ + { key: 1, name: 'PostgreSQL', type: 'Database', status: 'Active' }, + { key: 2, name: 'MySQL', type: 'Database', status: 'Active' }, + { key: 3, name: 'SQLite', type: 'Database', status: 'Inactive' }, + { key: 4, name: 'Presto', type: 'Query Engine', status: 'Active' }, + ]; + const columns = [ + { title: 'Name', dataIndex: 'name', key: 'name', width: 150 }, + { title: 'Type', dataIndex: 'type', key: 'type' }, + { title: 'Status', dataIndex: 'status', key: 'status', width: 100 }, + ]; + return
; +}`, + examples: [ + { + title: 'With Pagination', + code: `function PaginatedTable() { + const data = Array.from({ length: 20 }, (_, i) => ({ + key: i, + name: 'Record ' + (i + 1), + value: Math.round(Math.random() * 1000), + category: ['A', 'B', 'C'][i % 3], + })); + const columns = [ + { title: 'Name', dataIndex: 'name', key: 'name' }, + { title: 'Value', dataIndex: 'value', key: 'value', width: 100 }, + { title: 'Category', dataIndex: 'category', key: 'category', width: 100 }, + ]; + return ( +
+ ); +}`, + }, + { + title: 'Loading State', + code: `function LoadingTable() { + const columns = [ + { title: 'Name', dataIndex: 'name', key: 'name' }, + { title: 'Status', dataIndex: 'status', key: 'status' }, + ]; + return
; +}`, + }, + ], + }, +}; + const bigColumns: ColumnsType = [ { title: 'Name', diff --git a/superset-frontend/packages/superset-ui-core/src/components/Table/TableOverview.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Table/TableOverview.stories.tsx index acbb24fffbf1..b081d23d06cc 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Table/TableOverview.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Table/TableOverview.stories.tsx @@ -19,7 +19,7 @@ import Markdown from 'markdown-to-jsx'; export default { - title: 'Design System/Components/Table"', + title: 'Design System/Components/Table/TableOverview', }; export const Overview = () => ( diff --git a/superset-frontend/packages/superset-ui-core/src/components/TableView/TableView.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/TableView/TableView.stories.tsx index 08c131060e1c..7f476eaa8759 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/TableView/TableView.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/TableView/TableView.stories.tsx @@ -21,6 +21,14 @@ import { TableView, TableViewProps, EmptyWrapperType } from '.'; export default { title: 'Components/TableView', component: TableView, + parameters: { + docs: { + description: { + component: + 'A data table component with sorting, pagination, text wrapping, and empty state support. Built on react-table.', + }, + }, + }, }; export const InteractiveTableView = (args: TableViewProps) => ( @@ -67,7 +75,7 @@ InteractiveTableView.args = { 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam id porta neque, a vehicula orci. Maecenas rhoncus elit sit amet purus convallis placerat in at nunc. Nulla nec viverra augue.', }, { - id: 321, + id: 456, age: 10, name: 'John Smith', summary: @@ -76,7 +84,7 @@ InteractiveTableView.args = { ], initialSortBy: [{ id: 'name', desc: true }], noDataText: 'No data here', - pageSize: 1, + pageSize: 2, showRowCount: true, withPagination: true, columnsForWrapText: ['Summary'], @@ -84,22 +92,147 @@ InteractiveTableView.args = { }; InteractiveTableView.argTypes = { + pageSize: { + control: { type: 'number', min: 1 }, + description: 'Number of rows displayed per page.', + }, + withPagination: { + control: 'boolean', + description: 'Whether to show pagination controls below the table.', + }, + showRowCount: { + control: 'boolean', + description: 'Whether to display the total row count alongside pagination.', + }, + noDataText: { + control: 'text', + description: 'Text displayed when the table has no data.', + }, + scrollTopOnPagination: { + control: 'boolean', + description: + 'Whether to scroll to the top of the table when changing pages.', + }, emptyWrapperType: { - control: { - type: 'select', - }, + control: { type: 'select' }, options: [EmptyWrapperType.Default, EmptyWrapperType.Small], - }, - pageSize: { - control: { - type: 'number', - min: 1, - }, + description: 'Style of the empty state wrapper.', }, initialPageIndex: { - control: { - type: 'number', - min: 0, + control: { type: 'number', min: 0 }, + description: 'Initial page to display (zero-based).', + }, +}; + +InteractiveTableView.parameters = { + docs: { + staticProps: { + columns: [ + { accessor: 'id', Header: 'ID', sortable: true, id: 'id' }, + { accessor: 'age', Header: 'Age', id: 'age' }, + { accessor: 'name', Header: 'Name', id: 'name' }, + { accessor: 'summary', Header: 'Summary', id: 'summary' }, + ], + data: [ + { + id: 123, + age: 27, + name: 'Emily', + summary: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, + { + id: 321, + age: 10, + name: 'Kate', + summary: 'Nam id porta neque, a vehicula orci.', + }, + { + id: 456, + age: 10, + name: 'John Smith', + summary: 'Maecenas rhoncus elit sit amet purus convallis placerat.', + }, + ], }, + liveExample: `function Demo() { + return ( + + ); +}`, + examples: [ + { + title: 'Without Pagination', + code: `function NoPaginationDemo() { + return ( + + ); +}`, + }, + { + title: 'Empty State', + code: `function EmptyDemo() { + return ( + + ); +}`, + }, + { + title: 'With Sorting', + code: `function SortingDemo() { + return ( + + ); +}`, + }, + ], }, }; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Tabs/Tabs.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Tabs/Tabs.stories.tsx index 9cc87fcc6a62..dd35f72a1a68 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Tabs/Tabs.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Tabs/Tabs.stories.tsx @@ -21,61 +21,143 @@ import Tabs, { TabsProps } from '.'; export default { title: 'Components/Tabs', component: Tabs, + parameters: { + docs: { + description: { + component: + 'A tabs component for switching between different views or content sections. ' + + 'Supports multiple tab styles, positions, and sizes.', + }, + }, + }, }; -export const InteractiveTabs = (args: TabsProps) => ; +// Demo tab items (kept separate from args to avoid parser issues) +const demoItems = [ + { key: '1', label: 'Tab 1', children: 'Content of Tab Pane 1' }, + { key: '2', label: 'Tab 2', children: 'Content of Tab Pane 2' }, + { key: '3', label: 'Tab 3', children: 'Content of Tab Pane 3' }, +]; + +export const InteractiveTabs = (args: TabsProps) => ( + +); InteractiveTabs.args = { defaultActiveKey: '1', - animated: true, - centered: false, - allowOverflow: false, + type: 'line', tabPosition: 'top', size: 'middle', + animated: true, + centered: false, tabBarGutter: 8, - items: [ - { - key: '1', - label: 'Tab 1', - children: 'Content of Tab Pane 1', - }, - { - key: '2', - label: 'Tab 2', - children: 'Content of Tab Pane 2', - }, - { - key: '3', - label: 'Tab 3', - children: 'Content of Tab Pane 3', - }, - ], }; InteractiveTabs.argTypes = { onChange: { action: 'onChange' }, type: { - defaultValue: 'line', - control: { - type: 'inline-radio', - }, + description: 'The style of tabs. Options: line, card, editable-card.', + control: { type: 'inline-radio' }, options: ['line', 'card', 'editable-card'], }, tabPosition: { - control: { - type: 'inline-radio', - }, + description: 'Position of tabs. Options: top, bottom, left, right.', + control: { type: 'inline-radio' }, options: ['top', 'bottom', 'left', 'right'], }, size: { - control: { - type: 'inline-radio', - }, + description: 'Size of the tabs.', + control: { type: 'inline-radio' }, options: ['small', 'middle', 'large'], }, + animated: { + description: 'Whether to animate tab transitions.', + control: { type: 'boolean' }, + }, + centered: { + description: 'Whether to center the tabs.', + control: { type: 'boolean' }, + }, tabBarGutter: { - control: { - type: 'number', + description: 'The gap between tabs.', + control: { type: 'number' }, + }, +}; + +InteractiveTabs.parameters = { + docs: { + staticProps: { + items: [ + { key: '1', label: 'Tab 1', children: 'Content of Tab Pane 1' }, + { key: '2', label: 'Tab 2', children: 'Content of Tab Pane 2' }, + { key: '3', label: 'Tab 3', children: 'Content of Tab Pane 3' }, + ], }, + liveExample: `function Demo() { + return ( + + ); +}`, + examples: [ + { + title: 'Card Style', + code: `function CardTabs() { + return ( + + ); +}`, + }, + { + title: 'Tab Positions', + code: `function TabPositions() { + const items = [ + { key: '1', label: 'Tab 1', children: 'Content 1' }, + { key: '2', label: 'Tab 2', children: 'Content 2' }, + { key: '3', label: 'Tab 3', children: 'Content 3' }, + ]; + return ( +
+ {['top', 'bottom', 'left', 'right'].map(pos => ( +
+

{pos}

+ +
+ ))} +
+ ); +}`, + }, + { + title: 'With Icons', + code: `function IconTabs() { + return ( + Dashboards, children: 'Dashboard content here.' }, + { key: '2', label: <> Charts, children: 'Chart content here.' }, + { key: '3', label: <> Datasets, children: 'Dataset content here.' }, + { key: '4', label: <> SQL Lab, children: 'SQL Lab content here.' }, + ]} + /> + ); +}`, + }, + ], }, }; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Timer/Timer.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Timer/Timer.stories.tsx index e64567a100b2..a42d0bc2eeb4 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Timer/Timer.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Timer/Timer.stories.tsx @@ -22,30 +22,31 @@ import { Timer, TimerProps } from '.'; export default { title: 'Components/Timer', component: Timer, + parameters: { + docs: { + description: { + component: + 'A live elapsed-time display that counts up from a given start time. Used to show query and dashboard load durations. Requires a startTime timestamp to function.', + }, + }, + }, }; export const InteractiveTimer = (args: TimerProps) => ; InteractiveTimer.args = { - isRunning: false, + isRunning: true, + status: 'success', }; InteractiveTimer.argTypes = { - startTime: { - defaultValue: extendedDayjs().utc().valueOf(), - table: { - disable: true, - }, - }, - endTime: { - table: { - disable: true, - }, + isRunning: { + control: 'boolean', + description: + 'Whether the timer is actively counting. Toggle to start/stop.', }, status: { - control: { - type: 'select', - }, + control: { type: 'select' }, options: [ 'success', 'warning', @@ -55,11 +56,99 @@ InteractiveTimer.argTypes = { 'primary', 'secondary', ], + description: 'Visual status of the timer badge.', + }, + startTime: { + defaultValue: extendedDayjs().utc().valueOf(), + table: { disable: true }, + }, + endTime: { + table: { disable: true }, }, }; InteractiveTimer.parameters = { - actions: { - disabled: true, + actions: { disabled: true }, + docs: { + staticProps: { + startTime: 1737936000000, + }, + liveExample: `function Demo() { + const [isRunning, setIsRunning] = React.useState(true); + const [startTime] = React.useState(Date.now()); + return ( +
+ + +
+ ); +}`, + examples: [ + { + title: 'Status Variants', + code: `function StatusVariants() { + const [startTime] = React.useState(Date.now()); + return ( +
+ {['success', 'warning', 'danger', 'info', 'default', 'primary', 'secondary'].map(status => ( +
+ {status} + +
+ ))} +
+ ); +}`, + }, + { + title: 'Completed Timer', + code: `function CompletedTimer() { + const start = Date.now() - 5230; + const end = Date.now(); + return ( +
+ + Query completed in ~5.2 seconds +
+ ); +}`, + }, + { + title: 'Start and Stop', + code: `function StartStop() { + const [isRunning, setIsRunning] = React.useState(false); + const [startTime, setStartTime] = React.useState(null); + const handleToggle = () => { + if (!isRunning && !startTime) { + setStartTime(Date.now()); + } + setIsRunning(r => !r); + }; + return ( +
+ + +
+ ); +}`, + }, + ], }, }; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Tooltip/Tooltip.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Tooltip/Tooltip.stories.tsx index 0e0fa1ef1b68..bb04a26b31ed 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Tooltip/Tooltip.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Tooltip/Tooltip.stories.tsx @@ -18,30 +18,13 @@ */ import { Button } from '../Button'; import { Tooltip } from '.'; -import { TooltipPlacement, TooltipProps } from './types'; +import { TooltipProps } from './types'; export default { title: 'Components/Tooltip', component: Tooltip, }; -const PLACEMENTS: TooltipPlacement[] = [ - 'bottom', - 'bottomLeft', - 'bottomRight', - 'left', - 'leftBottom', - 'leftTop', - 'right', - 'rightBottom', - 'rightTop', - 'top', - 'topLeft', - 'topRight', -]; - -const TRIGGERS = ['hover', 'focus', 'click', 'contextMenu']; - export const InteractiveTooltip = (args: TooltipProps) => ( @@ -55,16 +38,98 @@ InteractiveTooltip.args = { }; InteractiveTooltip.argTypes = { + title: { + control: { type: 'text' }, + description: 'Text or content shown in the tooltip.', + }, placement: { - defaultValue: 'top', control: { type: 'select' }, - options: PLACEMENTS, + options: [ + 'bottom', + 'bottomLeft', + 'bottomRight', + 'left', + 'leftBottom', + 'leftTop', + 'right', + 'rightBottom', + 'rightTop', + 'top', + 'topLeft', + 'topRight', + ], + description: 'Position of the tooltip relative to the trigger element.', }, trigger: { - defaultValue: 'hover', control: { type: 'select' }, - options: TRIGGERS, + options: ['hover', 'focus', 'click', 'contextMenu'], + description: 'How the tooltip is triggered.', + }, + mouseEnterDelay: { + control: { type: 'number' }, + description: 'Delay in seconds before showing the tooltip on hover.', + }, + mouseLeaveDelay: { + control: { type: 'number' }, + description: + 'Delay in seconds before hiding the tooltip after mouse leave.', + }, + color: { + control: { type: 'color' }, + description: 'Custom background color for the tooltip.', }, - color: { control: { type: 'color' } }, onVisibleChange: { action: 'onVisibleChange' }, }; + +InteractiveTooltip.parameters = { + docs: { + sampleChildren: [ + { + component: 'Button', + props: { children: 'Hover me' }, + }, + ], + liveExample: `function Demo() { + return ( + + + + ); +}`, + examples: [ + { + title: 'Placements', + code: `function Placements() { + const placements = ['top', 'bottom', 'left', 'right', 'topLeft', 'topRight', 'bottomLeft', 'bottomRight']; + return ( +
+ {placements.map(placement => ( + + + + ))} +
+ ); +}`, + }, + { + title: 'Trigger Types', + code: `function Triggers() { + return ( +
+ + + + + + + + + +
+ ); +}`, + }, + ], + }, +}; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Tree/Tree.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Tree/Tree.stories.tsx index 84aa0d614db1..334524fd7c9e 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Tree/Tree.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Tree/Tree.stories.tsx @@ -16,162 +16,23 @@ * specific language governing permissions and limitations * under the License. */ -import { Meta, StoryObj } from '@storybook/react'; +import { StoryObj } from '@storybook/react'; import { Icons } from '@superset-ui/core/components/Icons'; import Tree, { TreeProps, type TreeDataNode } from './index'; -const meta = { +export default { title: 'Components/Tree', component: Tree, - argTypes: { - autoExpandParent: { - control: 'boolean', - description: 'Whether to automatically expand a parent treeNode ', - table: { - category: 'Tree', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, - }, - checkable: { - control: 'boolean', - description: 'Add a Checkbox before the treeNodes', - table: { - category: 'Tree', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, - }, - checkStrictly: { - control: 'boolean', - description: - 'Check treeNode precisely; parent treeNode and children treeNodes are not associated', - table: { - category: 'Tree', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, - }, - defaultExpandAll: { - control: 'boolean', - description: 'Whether to expand all treeNodes by default', - table: { - category: 'Tree', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, - }, - defaultExpandParent: { - control: 'boolean', - description: 'If auto expand parent treeNodes when init ', - table: { - category: 'Tree', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, - }, - disabled: { - control: 'boolean', - description: 'Whether disabled the tree', - table: { - category: 'Tree', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, - }, - draggable: { - control: 'boolean', - description: - 'Specifies whether this Tree or the node is draggable. Use icon: false to disable drag handler icon', - table: { - category: 'Tree', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, - }, - height: { - control: 'number', - description: - 'Config virtual scroll height. Will not support horizontal scroll when enable this', - table: { - category: 'Tree', - type: { summary: 'number' }, - }, - }, - multiple: { - control: 'boolean', - description: 'Allows selecting multiple treeNodes ', - table: { - category: 'Tree', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, - }, - selectable: { - control: 'boolean', - description: 'Whether can be selected', - table: { - category: 'Tree', - type: { summary: 'boolean' }, - defaultValue: { summary: 'true' }, - }, - }, - showIcon: { - control: 'boolean', - description: - 'Controls whether to display the icon node, no default style', - table: { - category: 'Tree', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, - }, - showLine: { - control: 'boolean', - description: 'Shows a connecting line', - table: { - category: 'Tree', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, - }, - virtual: { - control: 'boolean', - description: 'Disable virtual scroll when set to false', - table: { - category: 'Tree', - type: { summary: 'boolean' }, - defaultValue: { summary: 'true' }, - }, - }, - // Exclude unwanted properties - defaultExpandedKeys: { - table: { - disable: true, - }, - }, - defaultSelectedKeys: { - table: { - disable: true, - }, - }, - treeData: { - table: { - disable: true, - }, - }, - }, parameters: { docs: { description: { component: - 'The Tree component is used to display hierarchical data in a tree structure. It allows for features such as selection, expansion, and drag-and-drop functionality.', + 'The Tree component is used to display hierarchical data in a tree structure. ' + + 'It allows for features such as selection, expansion, and drag-and-drop functionality.', }, }, }, -} as Meta; - -export default meta; +}; const treeData: TreeDataNode[] = [ { @@ -237,3 +98,174 @@ export const TreeStory: Story = { }, render: (args: TreeProps) => , }; + +// Interactive story with primitive args for documentation +export const InteractiveTree = (args: TreeProps) => ( + +); + +InteractiveTree.args = { + checkable: false, + defaultExpandAll: false, + disabled: false, + draggable: false, + multiple: false, + selectable: true, + showIcon: false, + showLine: false, +}; + +InteractiveTree.argTypes = { + checkable: { + description: 'Add a Checkbox before the treeNodes', + control: { type: 'boolean' }, + }, + defaultExpandAll: { + description: 'Whether to expand all treeNodes by default', + control: { type: 'boolean' }, + }, + disabled: { + description: 'Whether disabled the tree', + control: { type: 'boolean' }, + }, + draggable: { + description: 'Specifies whether this Tree or the node is draggable', + control: { type: 'boolean' }, + }, + multiple: { + description: 'Allows selecting multiple treeNodes', + control: { type: 'boolean' }, + }, + selectable: { + description: 'Whether can be selected', + control: { type: 'boolean' }, + }, + showIcon: { + description: 'Controls whether to display the icon node', + control: { type: 'boolean' }, + }, + showLine: { + description: 'Shows a connecting line', + control: { type: 'boolean' }, + }, +}; + +InteractiveTree.parameters = { + docs: { + staticProps: { + treeData: [ + { + title: 'parent 1', + key: '0-0', + children: [ + { + title: 'parent 1-0', + key: '0-0-0', + children: [ + { title: 'leaf', key: '0-0-0-0' }, + { title: 'leaf', key: '0-0-0-1' }, + { title: 'leaf', key: '0-0-0-2' }, + ], + }, + { + title: 'parent 1-1', + key: '0-0-1', + children: [{ title: 'leaf', key: '0-0-1-0' }], + }, + { + title: 'parent 1-2', + key: '0-0-2', + children: [ + { title: 'leaf', key: '0-0-2-0' }, + { title: 'leaf', key: '0-0-2-1' }, + ], + }, + ], + }, + ], + defaultExpandedKeys: ['0-0', '0-0-0'], + }, + liveExample: `function Demo() { + const treeData = [ + { + title: 'Databases', + key: 'databases', + children: [ + { title: 'PostgreSQL', key: 'postgres' }, + { title: 'MySQL', key: 'mysql' }, + { title: 'SQLite', key: 'sqlite' }, + ], + }, + { + title: 'Charts', + key: 'charts', + children: [ + { title: 'Bar Chart', key: 'bar' }, + { title: 'Line Chart', key: 'line' }, + { title: 'Pie Chart', key: 'pie' }, + ], + }, + ]; + return ; +}`, + examples: [ + { + title: 'Checkable Tree', + code: `function CheckableTree() { + const [checkedKeys, setCheckedKeys] = React.useState(['postgres']); + const treeData = [ + { + title: 'Databases', + key: 'databases', + children: [ + { title: 'PostgreSQL', key: 'postgres' }, + { title: 'MySQL', key: 'mysql' }, + ], + }, + { + title: 'Charts', + key: 'charts', + children: [ + { title: 'Bar Chart', key: 'bar' }, + { title: 'Line Chart', key: 'line' }, + ], + }, + ]; + return ( + + ); +}`, + }, + { + title: 'With Lines and Icons', + code: `function LinesAndIcons() { + const treeData = [ + { + title: 'Dashboards', + key: 'dashboards', + children: [ + { title: 'Sales Dashboard', key: 'sales' }, + { title: 'Marketing Dashboard', key: 'marketing' }, + ], + }, + { + title: 'Reports', + key: 'reports', + children: [ + { title: 'Weekly Report', key: 'weekly' }, + { title: 'Monthly Report', key: 'monthly' }, + ], + }, + ]; + return ; +}`, + }, + ], + }, +}; diff --git a/superset-frontend/packages/superset-ui-core/src/components/TreeSelect/TreeSelect.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/TreeSelect/TreeSelect.stories.tsx index 7c0f6e33e0ac..da6a9cd778d4 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/TreeSelect/TreeSelect.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/TreeSelect/TreeSelect.stories.tsx @@ -22,199 +22,6 @@ import { TreeSelect, type TreeSelectProps } from '.'; export default { title: 'Components/TreeSelect', component: TreeSelect, - argTypes: { - allowClear: { - control: { type: 'boolean' }, - description: 'Whether to allow clearing the selected value.', - defaultValue: false, - table: { - category: 'TreeSelect', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, - }, - autoClearSearchValue: { - control: { type: 'boolean' }, - description: 'Whether to clear the search value automatically.', - defaultValue: false, - table: { - category: 'TreeSelect', - type: { summary: 'boolean' }, - defaultValue: { summary: 'true' }, - }, - }, - disabled: { - control: { type: 'boolean' }, - description: 'Whether the component is disabled.', - defaultValue: false, - table: { - category: 'TreeSelect', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, - }, - labelInValue: { - control: { type: 'boolean' }, - description: 'Whether to use label in value.', - defaultValue: false, - table: { - category: 'TreeSelect', - type: { summary: 'boolean' }, - }, - }, - listHeight: { - control: { type: 'number' }, - description: 'Height of the dropdown list.', - defaultValue: 256, - table: { - category: 'TreeSelect', - type: { summary: 'number' }, - defaultValue: { summary: '256' }, - }, - }, - maxTagCount: { - control: { type: 'number' }, - description: 'Maximum number of tags to display.', - table: { - category: 'TreeSelect', - type: { summary: 'number' }, - }, - }, - maxTagTextLength: { - control: { type: 'number' }, - description: 'Maximum length of tag text.', - defaultValue: 20, - table: { - category: 'TreeSelect', - type: { summary: 'number' }, - }, - }, - multiple: { - control: { type: 'boolean' }, - description: 'Whether to allow multiple selections.', - defaultValue: false, - table: { - category: 'TreeSelect', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, - }, - placeholder: { - control: { type: 'text' }, - description: 'Placeholder text for the input field.', - defaultValue: 'Please select', - table: { - category: 'TreeSelect', - type: { summary: 'string' }, - }, - }, - placement: { - control: { type: 'select' }, - options: ['topLeft', 'topRight', 'bottomLeft', 'bottomRight'], - description: 'Placement of the dropdown menu.', - defaultValue: 'bottomLeft', - table: { - category: 'TreeSelect', - type: { summary: 'string' }, - defaultValue: { summary: 'bottomLeft' }, - }, - }, - showSearch: { - control: { type: 'boolean' }, - description: 'Whether to show the search input.', - defaultValue: false, - table: { - category: 'TreeSelect', - type: { summary: 'boolean' }, - }, - }, - size: { - control: { type: 'select' }, - options: ['large', 'middle', 'small'], - description: 'Size of the component.', - defaultValue: 'middle', - table: { - category: 'TreeSelect', - type: { summary: 'string' }, - }, - }, - status: { - control: { type: 'select' }, - options: ['error', 'warning'], - description: 'Status of the component.', - defaultValue: 'error', - table: { - category: 'TreeSelect', - type: { summary: 'string' }, - }, - }, - treeCheckable: { - control: { type: 'boolean' }, - description: 'Whether to show checkable tree nodes.', - defaultValue: false, - table: { - category: 'TreeSelect', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, - }, - treeDefaultExpandAll: { - control: { type: 'boolean' }, - description: 'Whether to expand all tree nodes by default.', - defaultValue: false, - table: { - category: 'TreeSelect', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, - }, - treeIcon: { - control: { type: 'boolean' }, - description: 'Whether to show tree icons.', - defaultValue: false, - table: { - category: 'TreeSelect', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, - }, - treeLine: { - control: { type: 'boolean' }, - description: 'Whether to show tree lines.', - defaultValue: false, - table: { - category: 'TreeSelect', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, - }, - variant: { - control: { type: 'select' }, - options: ['outlined', 'borderless', 'filled', 'underlined'], - description: 'Variant of the component.', - defaultValue: 'outlined', - table: { - category: 'TreeSelect', - type: { summary: 'string' }, - defaultValue: { summary: 'outlined' }, - }, - }, - virtual: { - control: { type: 'boolean' }, - description: 'Whether to use virtual scrolling.', - defaultValue: false, - table: { - category: 'TreeSelect', - type: { summary: 'boolean' }, - defaultValue: { summary: 'true' }, - }, - }, - treeData: { - table: { - disable: true, - }, - }, - }, parameters: { docs: { description: { @@ -254,6 +61,199 @@ const treeData = [ }, ]; +export const InteractiveTreeSelect = (args: TreeSelectProps) => ( +
+ +
+); + +InteractiveTreeSelect.args = { + allowClear: true, + disabled: false, + multiple: false, + placeholder: 'Please select', + showSearch: true, + size: 'middle', + treeCheckable: false, + treeDefaultExpandAll: true, + treeLine: false, + variant: 'outlined', +}; + +InteractiveTreeSelect.argTypes = { + allowClear: { + control: { type: 'boolean' }, + description: 'Whether to allow clearing the selected value.', + }, + disabled: { + control: { type: 'boolean' }, + description: 'Whether the component is disabled.', + }, + multiple: { + control: { type: 'boolean' }, + description: 'Whether to allow multiple selections.', + }, + placeholder: { + control: { type: 'text' }, + description: 'Placeholder text for the input field.', + }, + showSearch: { + control: { type: 'boolean' }, + description: 'Whether to show the search input.', + }, + size: { + control: { type: 'select' }, + options: ['large', 'middle', 'small'], + description: 'Size of the component.', + }, + treeCheckable: { + control: { type: 'boolean' }, + description: 'Whether to show checkable tree nodes.', + }, + treeDefaultExpandAll: { + control: { type: 'boolean' }, + description: 'Whether to expand all tree nodes by default.', + }, + treeLine: { + control: { type: 'boolean' }, + description: 'Whether to show tree lines.', + }, + variant: { + control: { type: 'select' }, + options: ['outlined', 'borderless', 'filled'], + description: 'Variant of the component.', + }, + treeData: { + table: { disable: true }, + }, +}; + +InteractiveTreeSelect.parameters = { + docs: { + staticProps: { + treeData: [ + { + title: 'Node1', + value: '0-0', + children: [ + { title: 'Child Node1', value: '0-0-0' }, + { title: 'Child Node2', value: '0-0-1' }, + ], + }, + { + title: 'Node2', + value: '0-1', + children: [{ title: 'Child Node3', value: '0-1-0' }], + }, + ], + }, + liveExample: `function Demo() { + const [value, setValue] = React.useState(undefined); + const treeData = [ + { + title: 'Databases', + value: 'databases', + children: [ + { title: 'PostgreSQL', value: 'postgres' }, + { title: 'MySQL', value: 'mysql' }, + ], + }, + { + title: 'Charts', + value: 'charts', + children: [ + { title: 'Bar Chart', value: 'bar' }, + { title: 'Line Chart', value: 'line' }, + ], + }, + ]; + return ( + + ); +}`, + examples: [ + { + title: 'Multiple Selection with Checkboxes', + code: `function MultiSelectTree() { + const [value, setValue] = React.useState([]); + const treeData = [ + { + title: 'Databases', + value: 'databases', + children: [ + { title: 'PostgreSQL', value: 'postgres' }, + { title: 'MySQL', value: 'mysql' }, + { title: 'SQLite', value: 'sqlite' }, + ], + }, + { + title: 'File Formats', + value: 'formats', + children: [ + { title: 'CSV', value: 'csv' }, + { title: 'Excel', value: 'excel' }, + ], + }, + ]; + return ( + + ); +}`, + }, + { + title: 'With Tree Lines', + code: `function TreeLinesDemo() { + const treeData = [ + { + title: 'Dashboards', + value: 'dashboards', + children: [ + { title: 'Sales Dashboard', value: 'sales' }, + { title: 'Marketing Dashboard', value: 'marketing' }, + ], + }, + { + title: 'Charts', + value: 'charts', + children: [ + { title: 'Revenue Chart', value: 'revenue' }, + { title: 'User Growth', value: 'growth' }, + ], + }, + ]; + return ( + + ); +}`, + }, + ], + }, +}; + +// Keep original for backwards compatibility export const Default: Story = { args: { treeData, diff --git a/superset-frontend/packages/superset-ui-core/src/components/Typography/Typography.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Typography/Typography.stories.tsx index 16700be76f09..902119fd2e4d 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Typography/Typography.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Typography/Typography.stories.tsx @@ -28,146 +28,171 @@ export default { Paragraph: Typography.Paragraph, Link: Typography.Link, }, + parameters: { + docs: { + description: { + component: + 'Typography is a component for displaying text with various styles and formats. It includes subcomponents like Title, Paragraph, and Link.', + }, + }, + }, +} as Meta< + typeof Typography.Text & + typeof Typography.Paragraph & + typeof Typography.Link & + typeof Typography.Title +>; + +type TextStory = StoryObj; + +export const InteractiveTypography: TextStory = { + args: { + children: 'Sample Text', + code: false, + copyable: false, + delete: false, + disabled: false, + ellipsis: false, + keyboard: false, + mark: false, + italic: false, + underline: false, + strong: false, + type: undefined, + }, argTypes: { + children: { + control: 'text', + description: 'The text content.', + }, code: { control: 'boolean', - description: 'Code style', - table: { - category: 'Typography.Text', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, + description: 'Code style.', }, copyable: { control: 'boolean', - description: 'Whether to be copyable, customize it via setting an object', - table: { - category: 'Typography.Text', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, + description: 'Whether the text is copyable.', }, delete: { control: 'boolean', - description: 'Deleted line style', - table: { - category: 'Typography.Text', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, + description: 'Deleted line style.', }, disabled: { control: 'boolean', - description: 'Disabled content', - table: { - category: 'Typography.Text', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, - }, - editable: { - control: 'boolean', - description: 'Whether to be editable, customize it via setting an object', - table: { - category: 'Typography.Text', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, + description: 'Disabled content.', }, ellipsis: { control: 'boolean', - description: - 'Display ellipsis when text overflows, can not configure expandable、rows and onExpand by using object. Diff with Typography.Paragraph, Text do not have 100% width style which means it will fix width on the first ellipsis. If you want to have responsive ellipsis, please set width manually', - table: { - category: 'Typography.Text', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, + description: 'Display ellipsis when text overflows.', }, keyboard: { control: 'boolean', - description: 'Keyboard style', - table: { - category: 'Typography.Text', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, + description: 'Keyboard style.', }, mark: { control: 'boolean', - description: 'Marked style', - table: { - category: 'Typography.Text', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, + description: 'Marked/highlighted style.', }, italic: { control: 'boolean', - description: 'Italic style', - table: { - category: 'Typography.Text', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, - }, - type: { - control: 'select', - description: 'Text type', - options: ['secondary', 'success', 'warning', 'danger'], - table: { - category: 'Typography.Text', - type: { summary: 'string' }, - }, + description: 'Italic style.', }, underline: { control: 'boolean', - description: 'Underlined style ', - table: { - category: 'Typography.Text', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, - }, - level: { - control: { - type: 'number', - min: 1, - max: 5, - }, - description: 'Set content importance. Match with h1, h2, h3, h4, h5', - table: { - category: 'Typography.Title', - type: { summary: 'number' }, - }, + description: 'Underlined style.', }, strong: { control: 'boolean', - description: 'Bold style', - table: { - category: 'Typography.Paragraph', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, + description: 'Bold style.', }, - }, - parameters: { - docs: { - description: { - component: - 'Typography is a component for displaying text with various styles and formats. It includes subcomponents like Title, Paragraph, and Link.', - }, + type: { + control: 'select', + options: [undefined, 'secondary', 'success', 'warning', 'danger'], + description: 'Text type for semantic coloring.', }, }, -} as Meta< - typeof Typography.Text & - typeof Typography.Paragraph & - typeof Typography.Link & - typeof Typography.Title ->; + render: args => , +}; -type TextStory = StoryObj; +InteractiveTypography.parameters = { + docs: { + renderComponent: 'Typography.Text', + description: { + story: 'Text component with various styling options.', + }, + liveExample: `function Demo() { + return ( +
+ Default Text +
+ Secondary +
+ Success +
+ Warning +
+ Danger +
+ Code +
+ Keyboard +
+ Marked +
+ Underline +
+ Deleted +
+ Strong +
+ Italic +
+ ); +}`, + examples: [ + { + title: 'All Subcomponents', + code: `function AllSubcomponents() { + return ( +
+ Typography Components + + The Typography component includes several subcomponents for different text needs. + Use Title for headings, + Text for inline text styling, + and Paragraph for block content. + + + Learn more about Apache Superset + +
+ ); +}`, + }, + { + title: 'Text Styling Options', + code: `function TextStyles() { + return ( +
+ Code style + Keyboard style + Highlighted text + Underlined text + Deleted text + Bold text + Italic text + Success type + Warning type + Danger type +
+ ); +}`, + }, + ], + }, +}; +// Keep original for backwards compatibility export const TextStory: TextStory = { args: { children: 'Default Text', @@ -177,14 +202,169 @@ export const TextStory: TextStory = { type TitleStory = StoryObj; +export const InteractiveTitle: TitleStory = { + args: { + children: 'Sample Title', + level: 1, + copyable: false, + delete: false, + disabled: false, + ellipsis: false, + mark: false, + italic: false, + underline: false, + type: undefined, + }, + argTypes: { + children: { + control: 'text', + description: 'The title content.', + }, + level: { + control: { type: 'number', min: 1, max: 5 }, + description: 'Set content importance (h1-h5).', + }, + copyable: { + control: 'boolean', + description: 'Whether the title is copyable.', + }, + delete: { + control: 'boolean', + description: 'Deleted line style.', + }, + disabled: { + control: 'boolean', + description: 'Disabled content.', + }, + ellipsis: { + control: 'boolean', + description: 'Display ellipsis when text overflows.', + }, + mark: { + control: 'boolean', + description: 'Marked/highlighted style.', + }, + italic: { + control: 'boolean', + description: 'Italic style.', + }, + underline: { + control: 'boolean', + description: 'Underlined style.', + }, + type: { + control: 'select', + options: [undefined, 'secondary', 'success', 'warning', 'danger'], + description: 'Title type for semantic coloring.', + }, + }, + render: args => , + parameters: { + docs: { + description: { + story: 'Title component with heading levels h1-h5.', + }, + liveExample: `function Demo() { + return ( +
+ h1. Heading + h2. Heading + h3. Heading + h4. Heading + h5. Heading +
+ ); +}`, + }, + }, +}; + export const TitleStory: TitleStory = { args: { children: 'Default Title', }, render: args => , }; + type ParagraphStory = StoryObj; +export const InteractiveParagraph: ParagraphStory = { + args: { + children: + 'This is a paragraph of text. Paragraphs are block-level elements that support various text styling options.', + copyable: false, + delete: false, + disabled: false, + ellipsis: false, + mark: false, + strong: false, + italic: false, + underline: false, + type: undefined, + }, + argTypes: { + children: { + control: 'text', + description: 'The paragraph content.', + }, + copyable: { + control: 'boolean', + description: 'Whether the paragraph is copyable.', + }, + delete: { + control: 'boolean', + description: 'Deleted line style.', + }, + disabled: { + control: 'boolean', + description: 'Disabled content.', + }, + ellipsis: { + control: 'boolean', + description: 'Display ellipsis when text overflows.', + }, + mark: { + control: 'boolean', + description: 'Marked/highlighted style.', + }, + strong: { + control: 'boolean', + description: 'Bold style.', + }, + italic: { + control: 'boolean', + description: 'Italic style.', + }, + underline: { + control: 'boolean', + description: 'Underlined style.', + }, + type: { + control: 'select', + options: [undefined, 'secondary', 'success', 'warning', 'danger'], + description: 'Paragraph type for semantic coloring.', + }, + }, + render: args => , + parameters: { + docs: { + description: { + story: 'Paragraph component for block-level text content.', + }, + liveExample: `function Demo() { + return ( + + This is a paragraph. Paragraphs are used for block-level text content. + They support features like bold, + italic, and + code styling. + + ); +}`, + }, + }, +}; + export const ParagraphStory: ParagraphStory = { args: { children: 'Default Paragraph', @@ -194,6 +374,92 @@ export const ParagraphStory: ParagraphStory = { type LinkStory = StoryObj; +export const InteractiveLink: LinkStory = { + args: { + children: 'Click here', + href: 'https://superset.apache.org', + target: '_blank', + copyable: false, + delete: false, + disabled: false, + ellipsis: false, + mark: false, + strong: false, + italic: false, + underline: false, + type: undefined, + }, + argTypes: { + children: { + control: 'text', + description: 'The link text.', + }, + href: { + control: 'text', + description: 'The URL the link points to.', + }, + target: { + control: 'select', + options: ['_blank', '_self', '_parent', '_top'], + description: 'Where to open the linked document.', + }, + copyable: { + control: 'boolean', + description: 'Whether the link is copyable.', + }, + delete: { + control: 'boolean', + description: 'Deleted line style.', + }, + disabled: { + control: 'boolean', + description: 'Disabled link.', + }, + ellipsis: { + control: 'boolean', + description: 'Display ellipsis when text overflows.', + }, + mark: { + control: 'boolean', + description: 'Marked/highlighted style.', + }, + strong: { + control: 'boolean', + description: 'Bold style.', + }, + italic: { + control: 'boolean', + description: 'Italic style.', + }, + underline: { + control: 'boolean', + description: 'Underlined style.', + }, + type: { + control: 'select', + options: [undefined, 'secondary', 'success', 'warning', 'danger'], + description: 'Link type for semantic coloring.', + }, + }, + render: args => , + parameters: { + docs: { + description: { + story: 'Link component for hyperlinks with text styling options.', + }, + liveExample: `function Demo() { + return ( +
+ + Apache Superset + +
+ ); +}`, + }, + }, +}; + export const LinkStory: LinkStory = { args: { children: 'Default Link', diff --git a/superset-frontend/packages/superset-ui-core/src/components/UnsavedChangesModal/UnsavedChangesModal.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/UnsavedChangesModal/UnsavedChangesModal.stories.tsx index 4742c4da8feb..73054a401acc 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/UnsavedChangesModal/UnsavedChangesModal.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/UnsavedChangesModal/UnsavedChangesModal.stories.tsx @@ -33,15 +33,71 @@ export const InteractiveUnsavedChangesModal = ( ); InteractiveUnsavedChangesModal.args = { - showModal: true, - onHide: () => {}, - handleSave: () => {}, - onConfirmNavigation: () => {}, + showModal: false, title: 'Unsaved Changes', }; InteractiveUnsavedChangesModal.argTypes = { + showModal: { + control: { type: 'boolean' }, + description: 'Whether the modal is visible.', + }, + title: { + control: { type: 'text' }, + description: 'Title text displayed in the modal header.', + }, onHide: { action: 'onHide' }, handleSave: { action: 'handleSave' }, onConfirmNavigation: { action: 'onConfirmNavigation' }, }; + +InteractiveUnsavedChangesModal.parameters = { + docs: { + triggerProp: 'showModal', + onHideProp: 'onHide,handleSave,onConfirmNavigation', + liveExample: `function Demo() { + const [show, setShow] = React.useState(false); + return ( +
+ + setShow(false)} + handleSave={() => { alert('Saved!'); setShow(false); }} + onConfirmNavigation={() => { alert('Discarded changes'); setShow(false); }} + title="Unsaved Changes" + > + If you don't save, changes will be lost. + +
+ ); +}`, + examples: [ + { + title: 'Custom Title', + code: `function CustomTitle() { + const [show, setShow] = React.useState(false); + return ( +
+ + setShow(false)} + handleSave={() => setShow(false)} + onConfirmNavigation={() => setShow(false)} + title="You have unsaved dashboard changes" + > + Your dashboard layout and filter changes have not been saved. + Do you want to save before leaving? + +
+ ); +}`, + }, + ], + }, +}; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Upload/Upload.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Upload/Upload.stories.tsx index 94b524958de3..d68b87ecf18d 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Upload/Upload.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Upload/Upload.stories.tsx @@ -16,104 +16,25 @@ * specific language governing permissions and limitations * under the License. */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { StoryObj } from '@storybook/react'; import { Icons } from '../Icons'; import { Button } from '../Button'; import { Upload } from '.'; -const meta: Meta = { +export default { title: 'Components/Upload', component: Upload, - argTypes: { - accept: { - control: false, - description: 'File types that can be accepted', - defaultValue: undefined, - type: 'string', - }, - action: { - control: 'text', - description: 'Uploading URL', - defaultValue: undefined, - type: 'string', - }, - name: { - control: false, - description: 'The name of uploading file', - table: { - type: { summary: 'string' }, - defaultValue: { summary: 'file' }, - }, - }, - multiple: { - control: 'boolean', - description: 'Support multiple file selection', - table: { - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, - }, - disabled: { - control: 'boolean', - description: 'Disable upload button', - table: { - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, - }, - listType: { - control: 'select', - options: ['text', 'picture', 'picture-card', 'picture-circle'], - description: 'Built-in stylesheets for file list display', - table: { - type: { summary: 'string' }, - defaultValue: { summary: 'text' }, - }, - }, - showUploadList: { - control: 'boolean', - description: - 'Whether to show default upload list, could be an object to specify extra, showPreviewIcon, showRemoveIcon, showDownloadIcon, removeIcon and downloadIcon individually upload list display', - table: { - type: { summary: 'boolean' }, - defaultValue: { summary: 'true' }, - }, - }, - method: { - control: false, - description: 'The HTTP method of upload request', - table: { - type: { summary: 'string' }, - defaultValue: { summary: 'post' }, - }, - }, - withCredentials: { - control: false, - description: 'Send cookies with ajax upload', - table: { - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, - }, - openFileDialogOnClick: { - control: 'boolean', - description: 'Click open file dialog', - table: { - type: { summary: 'boolean' }, - defaultValue: { summary: 'true' }, - }, - }, - progress: { - control: false, - description: 'Custom progress bar', - table: { - type: { summary: 'object' }, + parameters: { + docs: { + description: { + component: + 'Upload component for file selection and uploading. ' + + 'Supports drag-and-drop, multiple files, and different list display styles.', }, }, }, }; -export default meta; type Story = StoryObj; export const Default: Story = { @@ -130,3 +51,74 @@ export const Default: Story = { ), }; + +export const InteractiveUpload = (args: any) => ; + +InteractiveUpload.args = { + multiple: false, + disabled: false, + listType: 'text', + showUploadList: true, +}; + +InteractiveUpload.argTypes = { + multiple: { + control: { type: 'boolean' }, + description: 'Support multiple file selection.', + }, + disabled: { + control: { type: 'boolean' }, + description: 'Disable the upload button.', + }, + listType: { + control: { type: 'select' }, + options: ['text', 'picture', 'picture-card', 'picture-circle'], + description: 'Built-in style for the file list display.', + }, + showUploadList: { + control: { type: 'boolean' }, + description: 'Whether to show the upload file list.', + }, +}; + +InteractiveUpload.parameters = { + docs: { + sampleChildren: [ + { + component: 'Button', + props: { children: 'Click to Upload' }, + }, + ], + liveExample: `function Demo() { + return ( + + + + ); +}`, + examples: [ + { + title: 'Picture Card Style', + code: `function PictureCard() { + return ( + + + Upload + + ); +}`, + }, + { + title: 'Drag and Drop', + code: `function DragDrop() { + return ( + +

+

+

Click or drag file to this area to upload

+

Support for single or bulk upload.

+
+ ); +}`, + }, + ], + }, +}; diff --git a/superset-frontend/packages/superset-ui-core/src/components/index.ts b/superset-frontend/packages/superset-ui-core/src/components/index.ts index 96d17aa65471..eecfdf90f882 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/index.ts +++ b/superset-frontend/packages/superset-ui-core/src/components/index.ts @@ -144,15 +144,31 @@ export { type ListViewCardProps, } from './ListViewCard'; export { Loading, type LoadingProps } from './Loading'; +export { default as MetadataBar, type MetadataBarProps } from './MetadataBar'; export { Progress, type ProgressProps } from './Progress'; +export { default as ProgressBar, type ProgressBarProps } from './ProgressBar'; export { Pagination, type PaginationProps } from './Pagination'; export { Skeleton, type SkeletonProps } from './Skeleton'; +export { + default as Slider, + type SliderSingleProps, + type SliderRangeProps, +} from './Slider'; export { Switch, type SwitchProps } from './Switch'; +export { + default as Tabs, + EditableTabs, + LineEditableTabs, + type TabsProps, +} from './Tabs'; + +export { default as Tree, type TreeProps, type TreeDataNode } from './Tree'; + export { TreeSelect, type TreeSelectProps } from './TreeSelect'; export { diff --git a/superset-frontend/packages/superset-ui-demo/README.md b/superset-frontend/packages/superset-ui-demo/README.md index b1a9be3a1fdd..c25c4706285a 100644 --- a/superset-frontend/packages/superset-ui-demo/README.md +++ b/superset-frontend/packages/superset-ui-demo/README.md @@ -17,55 +17,12 @@ specific language governing permissions and limitations under the License. --> -## @superset-ui/demo +# @superset-ui/demo -[![Version](https://img.shields.io/github/package-json/v/apache/superset?filename=superset-frontend%2Fpackages%2Fsuperset-ui-demo%2Fpackage.json&style=flat)](https://github.com/apache/superset/blob/master/superset-frontend/packages/superset-ui-demo/package.json) +Storybook for `@superset-ui` packages. -Storybook of `@superset-ui` packages. See it live at -[apache-superset.github.io/superset-ui](https://apache-superset.github.io/superset-ui) +**See it live:** [apache-superset.github.io/superset-ui](https://apache-superset.github.io/superset-ui) -### Development - -#### Run storybook - -To view the storybook locally, you should first run `npm ci && npm run bootstrap` in the -`@superset-ui` monorepo root directory, which will install all dependencies for this package and -sym-link any `@superset-ui` packages to your local system. - -After that run `npm run storybook` which will open up a dev server at http://localhost:9001. - -#### Adding new stories - -###### Existing package - -If stories already exist for the package you are adding, simply extend the `examples` already -exported for that package in the `storybook/stories//index.js` file. - -###### New package - -If you are creating stories for a package that doesn't yet have any stories, follow these steps: - -1. Add any new package dependencies (including any `@superset-ui/*` packages) via - `npm install `. - -2. Create a new folder that mirrors the package name - - > e.g., `mkdir storybook/stories/superset-ui-color/` - -3. Add an `index.js` file to that folder with a default export with the following shape: - -> you can use the `|` separator within the `storyPath` string to denote _nested_ stories e.g., -> `storyPath: '@superset-ui/package|Nested i|Nested ii'` - -```javascript - default export { - examples: [ - { - storyPath: , - storyName: , - renderStory: () => node, - }, - ... - ] - }; -``` +For documentation on running Storybook locally and adding stories, see the +[Storybook documentation](https://superset.apache.org/docs/developer_portal/testing/storybook) +in the Developer Portal.