docs(components): federate Storybook stories into Developer Portal MDX (#37502)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Evan Rusackas
2026-01-28 21:33:01 -08:00
committed by GitHub
parent 5fedb65bc0
commit 73e095db8e
79 changed files with 8112 additions and 1739 deletions

View File

@@ -75,6 +75,7 @@ logos/*
erd.puml
erd.svg
intro_header.txt
TODO.md
# for LLMs
llm-context.md

View File

@@ -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

View File

@@ -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 <MyComponent prop="value">Content</MyComponent>;
}`,
},
};
```
### 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/*/`

7
docs/.gitignore vendored
View File

@@ -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

View File

@@ -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'],
};

View File

@@ -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

View File

@@ -1,131 +0,0 @@
---
title: Alert
sidebar_label: Alert
---
<!--
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 { 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
<StoryWithControls
component={Alert}
props={{
closable: true,
type: 'info',
message: 'This is a sample alert message.',
description: 'Sample description for additional context.',
showIcon: true
}}
controls={[
{
name: 'type',
label: 'Type',
type: 'select',
options: [
'info',
'error',
'warning',
'success'
]
},
{
name: 'closable',
label: 'Closable',
type: 'boolean'
},
{
name: 'showIcon',
label: 'Show Icon',
type: 'boolean'
},
{
name: 'message',
label: 'Message',
type: 'text'
},
{
name: 'description',
label: 'Description',
type: 'text'
}
]}
/>
## Try It
Edit the code below to experiment with the component:
```tsx live
function Demo() {
return (
<Alert
closable
type="info"
message="This is a sample alert message."
description="Sample description for additional context."
showIcon
/>
);
}
```
## 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 (
<Alert
closable
type="info"
message="This is a sample alert message."
/>
);
}
```
## 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.*

View File

@@ -1,93 +0,0 @@
---
title: Extension Components
sidebar_label: Overview
sidebar_position: 1
---
<!--
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.
-->
# 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 (
<Alert type="info">
Welcome to my extension!
</Alert>
);
}
```
## 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) => <MyComponent {...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).

View File

@@ -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',
},
],
},
],
};

View File

@@ -0,0 +1,114 @@
---
title: Storybook
sidebar_position: 5
---
<!--
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.
-->
# 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/<package>/index.js
```
### To a New Package
1. Add package dependencies:
```bash
npm install <package>
```
2. Create a story folder matching the package name:
```bash
mkdir storybook/stories/superset-ui-<package>/
```
3. Create an `index.js` file with the story configuration:
```javascript
export default {
examples: [
{
storyPath: '@superset-ui/package',
storyName: 'My Story',
renderStory: () => <MyComponent />,
},
],
};
```
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

View File

@@ -132,6 +132,12 @@ if (!versionsConfig.developer_portal.disabled && !versionsConfig.developer_porta
docId: 'index',
label: 'Overview',
},
{
type: 'doc',
docsPluginId: 'developer_portal',
docId: 'contributing/overview',
label: 'Contributing',
},
{
type: 'doc',
docsPluginId: 'developer_portal',
@@ -147,14 +153,8 @@ if (!versionsConfig.developer_portal.disabled && !versionsConfig.developer_porta
{
type: 'doc',
docsPluginId: 'developer_portal',
docId: 'guidelines/design-guidelines',
label: 'Guidelines',
},
{
type: 'doc',
docsPluginId: 'developer_portal',
docId: 'contributing/overview',
label: 'Contributing',
docId: 'components/index',
label: 'UI Components',
},
{
label: 'API Reference',

View File

@@ -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]

View File

@@ -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",

File diff suppressed because it is too large Load Diff

View File

@@ -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',

View File

@@ -39,11 +39,12 @@ const StyledBlurredSection = styled('section')`
interface BlurredSectionProps {
children: ReactNode;
id?: string;
}
const BlurredSection = ({ children }: BlurredSectionProps) => {
const BlurredSection = ({ children, id }: BlurredSectionProps) => {
return (
<StyledBlurredSection>
<StyledBlurredSection id={id}>
{children}
<img className="blur" src="/img/community/blur.png" alt="Blur" />
</StyledBlurredSection>

View File

@@ -18,33 +18,245 @@
*/
import React from 'react';
import { supersetTheme, ThemeProvider } from '@superset-ui/core';
import BrowserOnly from '@docusaurus/BrowserOnly';
// A simple component to display a story example
export function StoryExample({ component: Component, props = {} }) {
// 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 }) => (
<themeObject.SupersetThemeProvider>
<ConfigProvider
getPopupContainer={getPopupContainer}
getTargetContainer={() => document.body}
>
<App>{children}</App>
</ConfigProvider>
</themeObject.SupersetThemeProvider>
);
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 (
<ThemeProvider theme={supersetTheme}>
<div
className="storybook-example"
style={{
border: '1px solid #e8e8e8',
borderRadius: '4px',
padding: '20px',
marginBottom: '20px',
}}
>
{Component && <Component {...props} />}
</div>
</ThemeProvider>
<div
style={{
border: '1px solid #e8e8e8',
borderRadius: '4px',
padding: '20px',
marginBottom: '20px',
minHeight: '100px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#999',
}}
>
Loading component...
</div>
);
}
// A simple component to display a story with controls
export function StoryWithControls({
component: Component,
props = {},
controls = [],
}) {
// A simple component to display a story example
export function StoryExample({ component, props = {} }) {
return (
<BrowserOnly fallback={<LoadingPlaceholder />}>
{() => {
const Component = resolveComponent(component);
const Providers = getProviders();
const { children, restProps } = extractChildren(props);
return (
<Providers>
<div
className="storybook-example"
style={{
border: '1px solid #e8e8e8',
borderRadius: '4px',
padding: '20px',
marginBottom: '20px',
position: 'relative', // Required for portal positioning
}}
>
{Component ? (
<Component {...restProps}>{children}</Component>
) : (
<div style={{ color: '#999' }}>
Component &quot;{String(component)}&quot; not found
</div>
)}
</div>
</Providers>
);
}}
</BrowserOnly>
);
}
// 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 <ChildComponent key={i} {...item.props} />;
}
// Fallback if component not found
return <div key={i}>{item.props?.children || `Unknown: ${item.component}`}</div>;
}
// Simple string
return (
<div key={i} style={itemStyle}>
{item}
</div>
);
});
}
// If it's a number, generate that many sample items
if (typeof sampleChildren === 'number') {
return new Array(sampleChildren).fill(null).map((_, i) => (
<div key={i} style={itemStyle}>
Item {i + 1}
</div>
));
}
// 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] = <PropComponent {...value.props} />;
}
}
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 (
<ThemeProvider theme={supersetTheme}>
<Providers>
<div className="storybook-with-controls">
<div
className="storybook-example"
@@ -64,9 +345,24 @@ export function StoryWithControls({
borderRadius: '4px',
padding: '20px',
marginBottom: '20px',
position: 'relative', // Required for portal positioning
}}
>
{Component && <Component {...stateProps} />}
{Component ? (
<>
{/* Show a trigger button for components like Modal */}
{triggerProp && ButtonComponent && (
<ButtonComponent onClick={() => updateProp(triggerProp, true)}>
Open {component}
</ButtonComponent>
)}
<Component {...filteredProps} {...triggerProps}>{children}</Component>
</>
) : (
<div style={{ color: '#999' }}>
Component &quot;{String(componentToRender)}&quot; not found
</div>
)}
</div>
{controls.length > 0 && (
@@ -87,26 +383,64 @@ export function StoryWithControls({
</label>
{control.type === 'select' ? (
<select
value={stateProps[control.name]}
onChange={e => updateProp(control.name, e.target.value)}
value={stateProps[control.name] ?? ''}
onChange={e => updateProp(control.name, e.target.value || undefined)}
style={{ width: '100%', padding: '5px' }}
>
{control.options.map(option => (
<option value=""> None </option>
{control.options?.map(option => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
) : control.type === 'inline-radio' || control.type === 'radio' ? (
<div style={{ display: 'flex', gap: '10px', flexWrap: 'wrap' }}>
{control.options?.map(option => (
<label
key={option}
style={{ display: 'flex', alignItems: 'center', gap: '4px' }}
>
<input
type="radio"
name={control.name}
value={option}
checked={stateProps[control.name] === option}
onChange={e => updateProp(control.name, e.target.value)}
/>
{option}
</label>
))}
</div>
) : control.type === 'boolean' ? (
<input
type="checkbox"
checked={stateProps[control.name]}
onChange={e => updateProp(control.name, e.target.checked)}
/>
) : control.type === 'number' ? (
<input
type="number"
value={stateProps[control.name]}
onChange={e => updateProp(control.name, Number(e.target.value))}
style={{ width: '100%', padding: '5px' }}
/>
) : control.type === 'color' ? (
<input
type="color"
value={stateProps[control.name] || '#000000'}
onChange={e => updateProp(control.name, e.target.value)}
style={{
width: '50px',
height: '30px',
padding: '2px',
cursor: 'pointer',
}}
/>
) : (
<input
type="text"
value={stateProps[control.name]}
value={stateProps[control.name] ?? ''}
onChange={e => updateProp(control.name, e.target.value)}
style={{ width: '100%', padding: '5px' }}
/>
@@ -116,6 +450,81 @@ export function StoryWithControls({
</div>
)}
</div>
</ThemeProvider>
</Providers>
);
}
// 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 (
<BrowserOnly fallback={<LoadingPlaceholder />}>
{() => (
<StoryWithControlsInner
component={Component}
renderComponent={renderComponent}
props={props}
controls={controls}
sampleChildren={sampleChildren}
sampleChildrenStyle={sampleChildrenStyle}
triggerProp={triggerProp}
onHideProp={onHideProp}
/>
)}
</BrowserOnly>
);
}
// Inner component for ComponentGallery (browser-only)
function ComponentGalleryInner({ component, sizes, styles, sizeProp, styleProp }) {
const Component = resolveComponent(component);
const Providers = getProviders();
if (!Component) {
return (
<div style={{ color: '#999' }}>
Component &quot;{String(component)}&quot; not found
</div>
);
}
return (
<Providers>
<div className="component-gallery">
{sizes.map(size => (
<div key={size} style={{ marginBottom: 40 }}>
<h4 style={{ marginBottom: 16, color: '#666' }}>{size}</h4>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '12px', alignItems: 'center' }}>
{styles.map(style => (
<Component
key={`${style}_${size}`}
{...{ [sizeProp]: size, [styleProp]: style }}
>
{style}
</Component>
))}
</div>
</div>
))}
</div>
</Providers>
);
}
// A component to display a gallery of all variants (sizes x styles)
export function ComponentGallery({ component, sizes = [], styles = [], sizeProp = 'size', styleProp = 'variant' }) {
return (
<BrowserOnly fallback={<LoadingPlaceholder />}>
{() => (
<ComponentGalleryInner
component={component}
sizes={sizes}
styles={styles}
sizeProp={sizeProp}
styleProp={styleProp}
/>
)}
</BrowserOnly>
);
}

View File

@@ -218,7 +218,7 @@ const Community = () => {
)}
</ul>
</StyledJoinCommunity>
<BlurredSection>
<BlurredSection id="superset-community-calendar">
<SectionHeader
level="h2"
title="Superset Community Calendar"

View File

@@ -0,0 +1,118 @@
/**
* 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.
*/
// Null module shim for packages not available in the docs build.
// These are transitive dependencies of superset-frontend components that exist
// in the barrel file but are never rendered on the docs site.
// webpack needs these to resolve at build time even though the code paths
// that use them are never executed at runtime.
//
// This shim uses a recursive Proxy to handle nested property access chains:
// import ace from 'ace-builds'; ace.config.set(...) → works (returns proxy)
// import { useResizeDetector } from 'react-resize-detector' → returns noop hook
// import ReactAce from 'react-ace' → returns NullComponent
const NullComponent = () => 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();

54
docs/src/shims/react-table.js vendored Normal file
View File

@@ -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;

View File

@@ -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;
}

10
docs/src/theme.d.ts vendored
View File

@@ -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;
}

View File

@@ -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 }) => (
<themeObject.SupersetThemeProvider>
<App>{children}</App>
</themeObject.SupersetThemeProvider>
);
} catch (e) {
console.error('[PlaygroundPreview] Failed to load theme provider:', e);
return ({ children }: { children: React.ReactNode }) => <>{children}</>;
}
}
function Loader() {
return <div>Loading...</div>;
}
function ThemedLivePreview(): ReactNode {
const ThemeWrapper = getThemeWrapper();
return (
<ThemeWrapper>
<LivePreview />
</ThemeWrapper>
);
}
function PlaygroundLivePreview(): ReactNode {
// No SSR for the live preview
// See https://github.com/facebook/docusaurus/issues/5747
return (
<BrowserOnly fallback={<Loader />}>
{() => (
<>
<ErrorBoundary
fallback={(params) => (
<ErrorBoundaryErrorMessageFallback {...params} />
)}
>
<ThemedLivePreview />
</ErrorBoundary>
<LiveError />
</>
)}
</BrowserOnly>
);
}
export default function PlaygroundPreview(): ReactNode {
return (
<>
<PlaygroundHeader>
<Translate
id="theme.Playground.result"
description="The result label of the live codeblocks"
>
Result
</Translate>
</PlaygroundHeader>
<div className={styles.playgroundPreview}>
<PlaygroundLivePreview />
</div>
</>
);
}

View File

@@ -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);
}

View File

@@ -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.:
* <Button>Click me</Button>
* <Avatar size="large" />
* <Badge count={5} />
*/
const ReactLiveScope = {
// Base scope with React (always available)
const ReactLiveScope: Record<string, unknown> = {
// React core
React,
...React,
// Extension components from @apache-superset/core
Alert,
// Common Ant Design components (for demos)
Button,
Card,
Input,
Space,
Tag,
Tooltip,
};
// 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');
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;

View File

@@ -18,6 +18,7 @@
*/
import path from 'path';
import webpack from 'webpack';
import type { Plugin } from '@docusaurus/types';
export default function webpackExtendPlugin(): Plugin<void> {
@@ -26,12 +27,73 @@ export default function webpackExtendPlugin(): Plugin<void> {
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<void> {
},
}),
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<void> {
__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(

BIN
docs/static/img/atomic-design.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

View File

@@ -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"

View File

@@ -217,6 +217,15 @@ export default {
},
} as Meta<typeof AutoComplete>;
// 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<AutoCompleteProps['options']>([]);
const handleSearch = (value: string) => {
@@ -252,16 +261,60 @@ const AutoCompleteWithOptions = (args: AutoCompleteProps) => {
return <AutoComplete {...args} options={options} onSearch={handleSearch} />;
};
type Story = StoryObj<typeof AutoComplete>;
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) => (
<div style={{ margin: '20px' }}>
<AutoCompleteWithOptions {...args} />
<AutoCompleteWithDynamicSearch {...args} />
</div>
),
parameters: {
docs: {
disable: true, // Hide from docs, it's a Storybook-specific demo
},
},
};

View File

@@ -27,6 +27,7 @@ export default {
export const InteractiveAvatar = (args: AvatarProps) => <Avatar {...args} />;
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 },
},
};

View File

@@ -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) => <Badge {...args} />;
// Count Badge - shows a number
export const InteractiveBadge = (args: BadgeProps) => (
<Badge {...args}>
<div
style={{
width: 40,
height: 40,
background: '#eee',
borderRadius: 4,
}}
/>
</Badge>
);
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 (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
{statuses.map(status => (
<Badge key={status} status={status} text={\`Status: \${status}\`} />
))}
</div>
);
}`,
},
{
title: 'Color Gallery',
code: `function ColorGallery() {
const colors = ['pink', 'red', 'orange', 'green', 'cyan', 'blue', 'purple'];
return (
<div style={{ display: 'flex', gap: 16, flexWrap: 'wrap' }}>
{colors.map(color => (
<Badge key={color} count={9} color={color} />
))}
</div>
);
}`,
},
],
},
};
// Status Badge - shows a status dot with text
export const StatusBadge = (args: BadgeProps) => <Badge {...args} />;
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 => (
<div key={size} style={{ marginBottom: 40 }}>
<h4>{size}</h4>
{COLORS.options.map(color => (
<Badge
count={9}
size={size}
key={`${color}_${size}`}
style={{ marginRight: '15px' }}
/>
))}
<div style={{ display: 'flex', gap: 24 }}>
{COLORS.options.map(color => (
<Badge
count={9}
color={color}
size={size}
key={`${color}_${size}`}
/>
))}
</div>
</div>
))}
</>

View File

@@ -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<typeof Breadcrumb>;
export const InteractiveBreadcrumb = (args: BreadcrumbProps) => (
<Breadcrumb {...args} />
);
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 (
<Breadcrumb
items={[
{ title: 'Home', href: '/' },
{ title: 'Library', href: '/library' },
{ title: 'Data' },
]}
separator="/"
/>
);
}`,
},
};
// Keep original for backwards compatibility
export const Default: Story = {
args: {
items: [
{ title: 'Home', href: '/' },
{ title: 'Library', href: '/library' },
{ title: 'Data' },
],
items: sampleItems,
},
render: args => <Breadcrumb {...args} />,
};

View File

@@ -100,18 +100,31 @@ ButtonGallery.parameters = {
},
};
export const InteractiveButton = (args: ButtonProps & { label: string }) => {
const { label, ...btnArgs } = args;
return <Button {...btnArgs}>{label}</Button>;
};
export const InteractiveButton = (args: ButtonProps & { children: string }) => (
<Button {...args} />
);
InteractiveButton.args = {
buttonStyle: 'default',
buttonSize: 'default',
label: 'Button!',
children: 'Button!',
};
InteractiveButton.argTypes = {
children: {
description: 'The button text or content.',
control: { type: 'text' },
},
buttonStyle: {
description: 'The style variant of the button.',
options: buttonStyles,
control: { type: 'select' },
},
buttonSize: {
description: 'The size of the button.',
options: buttonSizes,
control: { type: 'select' },
},
target: {
name: TARGETS.label,
control: { type: 'select' },
@@ -122,5 +135,26 @@ InteractiveButton.argTypes = {
control: { type: 'select' },
options: Object.values(HREFS.options),
},
disabled: {
description: 'Whether the button is disabled.',
control: { type: 'boolean' },
},
loading: {
description: 'Whether to show loading spinner.',
control: { type: 'boolean' },
},
onClick: { action: 'clicked' },
};
// Gallery configuration for docs - renders all button variants like ButtonGallery
InteractiveButton.parameters = {
docs: {
gallery: {
component: 'Button',
sizes: buttonSizes,
styles: buttonStyles,
sizeProp: 'buttonSize',
styleProp: 'buttonStyle',
},
},
};

View File

@@ -18,15 +18,86 @@
*/
import { Button } from '../Button';
import type { ButtonProps } from '../Button/types';
import { STYLES, SIZES } from '../Button/Button.stories';
import type { ButtonGroupProps } from './types';
import { ButtonGroup } from '.';
export default {
title: 'Components/ButtonGroup',
component: ButtonGroup,
parameters: {
docs: {
description: {
component:
'ButtonGroup is a container that groups multiple Button components together with consistent spacing and styling.',
},
},
},
};
export const InteractiveButtonGroup = (args: ButtonProps) => (
// Interactive demo showing ButtonGroup with its own props
export const InteractiveButtonGroup = (args: ButtonGroupProps) => (
<ButtonGroup {...args}>
<Button buttonStyle="tertiary">Button 1</Button>
<Button buttonStyle="tertiary">Button 2</Button>
<Button buttonStyle="tertiary">Button 3</Button>
</ButtonGroup>
);
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 (
<ButtonGroup>
<Button buttonStyle="tertiary">Button 1</Button>
<Button buttonStyle="tertiary">Button 2</Button>
<Button buttonStyle="tertiary">Button 3</Button>
</ButtonGroup>
);
}`,
},
};
// Gallery showing different button styles in groups
export const ButtonGroupGallery = (args: ButtonProps) => (
<>
<ButtonGroup css={{ marginBottom: 40 }}>
<Button {...args}>Button 1</Button>
@@ -42,25 +113,34 @@ export const InteractiveButtonGroup = (args: ButtonProps) => (
</ButtonGroup>
</>
);
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,
},

View File

@@ -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) => <Card {...args} />;
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 (
<Card title="Dashboard Overview" bordered>
This card displays a summary of your dashboard metrics and recent activity.
</Card>
);
}`,
examples: [
{
title: 'Card States',
code: `function CardStates() {
return (
<div style={{ display: 'flex', gap: 16, flexWrap: 'wrap' }}>
<Card title="Default" bordered style={{ width: 250 }}>
Default card content.
</Card>
<Card title="Hoverable" bordered hoverable style={{ width: 250 }}>
Hover over this card.
</Card>
<Card title="Loading" bordered loading style={{ width: 250 }}>
This content is hidden while loading.
</Card>
<Card title="No Border" style={{ width: 250 }}>
Borderless card.
</Card>
</div>
);
}`,
},
],
},
};

View File

@@ -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 (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<Checkbox checked={false}>Unchecked</Checkbox>
<Checkbox checked={true}>Checked</Checkbox>
<Checkbox indeterminate={true}>Indeterminate</Checkbox>
<Checkbox disabled>Disabled unchecked</Checkbox>
<Checkbox disabled checked>Disabled checked</Checkbox>
</div>
);
}`,
},
{
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 (
<div>
<Checkbox
checked={allSelected}
indeterminate={indeterminate}
onChange={(e) => setSelected(e.target.checked ? [...options] : [])}
>
Select All
</Checkbox>
<div style={{ marginLeft: 24, marginTop: 8 }}>
{options.map(opt => (
<div key={opt}>
<Checkbox
checked={selected.includes(opt)}
onChange={() => setSelected(prev =>
prev.includes(opt) ? prev.filter(x => x !== opt) : [...prev, opt]
)}
>
{opt}
</Checkbox>
</div>
))}
</div>
</div>
);
}`,
},
],
},
};
// All checkbox states including indeterminate
const STATES = [
{

View File

@@ -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 (
<DatePicker
placeholder="Select date"
format="YYYY-MM-DD hh:mm a"
showNow
showTime={{ format: 'hh:mm a', needConfirm: false }}
/>
);
}`,
},
};
export const InteractiveRangePicker = (
args: Omit<RangePickerProps, 'picker'> & {

View File

@@ -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 (
<>
<p>Horizontal divider with title (orientationMargin applies here):</p>
<Divider orientation="left" orientationMargin={0}>Left Title</Divider>
<Divider orientation="right" orientationMargin={50}>Right Title</Divider>
<Divider>Center Title</Divider>
<p>Vertical divider (use container gap for spacing):</p>
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
<span>Link</span>
<Divider type="vertical" />
<span>Link</span>
<Divider type="vertical" />
<span>Link</span>
</div>
</>
);
}`,
},
};

View File

@@ -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) => {
</div>
);
};
// Interactive story for docs generation
export const InteractiveDropdownContainer = (args: DropdownContainerProps) => {
const simpleItems = Array.from({ length: 6 }, (_, i) => ({
id: `item-${i}`,
element: (
<div
style={{
minWidth: 120,
padding: '4px 12px',
background: '#e6f4ff',
border: '1px solid #91caff',
borderRadius: 4,
}}
>
Filter {i + 1}
</div>
),
}));
return (
<div
style={{
width: 500,
resize: 'horizontal',
overflow: 'auto',
border: '1px solid #e8e8e8',
padding: 16,
}}
>
<DropdownContainer {...args} items={simpleItems} />
</div>
);
};
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 (
<div style={{ width: 400, resize: 'horizontal', overflow: 'auto', border: '1px solid #e8e8e8', padding: 16 }}>
<DropdownContainer items={items} />
<div style={{ marginTop: 8, color: '#999', fontSize: 12 }}>
Drag the right edge to resize and see items overflow into a dropdown
</div>
</div>
);
}`,
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 (
<div style={{ width: 500, resize: 'horizontal', overflow: 'auto', border: '1px solid #e8e8e8', padding: 16 }}>
<DropdownContainer items={items} />
</div>
);
}`,
},
],
},
};

View File

@@ -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 (
<EditableTitle
title="My Dashboard"
canEdit
showTooltip
certifiedBy="Data Team"
certificationDetails="Verified Q1 2024"
onSaveTitle={(newTitle) => console.log('Saved:', newTitle)}
/>
);
}`,
},
};

View File

@@ -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 => <EmptyState {...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 (
<EmptyState
size="medium"
title="No Results Found"
description="Try adjusting your filters or search terms."
image="filter.svg"
buttonText="Clear Filters"
buttonAction={() => alert('Filters cleared!')}
/>
);
}`,
},
};

View File

@@ -71,3 +71,46 @@ export const Default: Story = {
</div>
),
};
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 => (
<span style={{ display: 'inline-block' }}>
<FaveStar {...args} />
</span>
),
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 (
<FaveStar
itemId={1}
isStarred={starred}
showTooltip
saveFaveStar={toggle}
/>
);
}`,
},
},
};

View File

@@ -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) => (
<Flex
{...args}
@@ -34,8 +45,10 @@ export const InteractiveFlex = (args: FlexProps) => (
height: 90vh;
`}
>
{new Array(20).fill(null).map((_, i) => (
<p key={i}>Item</p>
{SAMPLE_ITEMS.map((item, i) => (
<div key={i} style={sampleItemStyle}>
{item}
</div>
))}
</Flex>
);
@@ -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 (
<Flex gap="small" wrap="wrap">
{['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5'].map(item => (
<div
key={item}
style={{
padding: '8px 16px',
background: '#e6f4ff',
border: '1px solid #91caff',
borderRadius: 4,
}}
>
{item}
</div>
))}
</Flex>
);
}`,
examples: [
{
title: 'Vertical Layout',
code: `function VerticalFlex() {
return (
<Flex vertical gap="small">
<Button buttonStyle="primary">Primary</Button>
<Button buttonStyle="dashed">Dashed</Button>
<Button buttonStyle="link">Link</Button>
</Flex>
);
}`,
},
{
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 (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
{['flex-start', 'center', 'flex-end', 'space-between', 'space-around'].map(justify => (
<div key={justify}>
<span style={{ marginBottom: 4, display: 'block', color: '#666' }}>{justify}</span>
<Flex style={boxStyle} justify={justify} align="center">
<div style={itemStyle} />
<div style={itemStyle} />
<div style={itemStyle} />
</Flex>
</div>
))}
</div>
);
}`,
},
],
},
};

View File

@@ -26,172 +26,6 @@ export default {
title: 'Design System/Components/Grid',
component: Row,
subcomponents: { Col },
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' },
},
},
justify: {
control: 'select',
options: [
'start',
'end',
'center',
'space-around',
'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' },
},
},
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' },
},
},
// 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: {
control: 'number',
description:
'Settings for small screens (≥ 576px). Can be a number (span) or object.',
table: {
category: 'Col',
type: { summary: 'number | object' },
},
},
md: {
control: 'number',
description:
'Settings for medium screens (≥ 768px). Can be a number (span) or object.',
table: {
category: 'Col',
type: { summary: 'number | object' },
},
},
lg: {
control: 'number',
description:
'Settings for large screens (≥ 992px). Can be a number (span) or object.',
table: {
category: 'Col',
type: { summary: 'number | object' },
},
},
xl: {
control: 'number',
description:
'Settings for extra-large screens (≥ 1200px). Can be a number (span) or object.',
table: {
category: 'Col',
type: { summary: 'number | object' },
},
},
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' },
},
},
},
parameters: {
docs: {
description: {
@@ -204,6 +38,230 @@ export default {
type Story = StoryObj<typeof Row>;
export const InteractiveGrid: Story = {
args: {
align: 'top',
justify: 'start',
wrap: true,
gutter: 16,
},
argTypes: {
align: {
control: 'select',
options: ['top', 'middle', 'bottom', 'stretch'],
description: 'Vertical alignment of columns within the row.',
},
justify: {
control: 'select',
options: [
'start',
'end',
'center',
'space-around',
'space-between',
'space-evenly',
],
description: 'Horizontal distribution of columns within the row.',
},
wrap: {
control: 'boolean',
description: 'Whether columns are allowed to wrap to the next line.',
},
gutter: {
control: 'number',
description: 'Spacing between columns in pixels.',
},
},
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) => (
<Col
key={i}
style={{
background: '#ddd',
padding: '8px',
}}
{...colProps}
>
Column {i + 1}
</Col>
));
return (
<div style={{ padding: '20px' }}>
<div style={{ marginBottom: '16px' }}>
<span>Horizontal Gutter: </span>
<Slider
min={8}
max={48}
step={8}
value={gutter}
onChange={setGutter}
/>
</div>
<div style={{ marginBottom: '16px' }}>
<span>Vertical Gutter: </span>
<Slider
min={8}
max={48}
step={8}
value={vgutter}
onChange={setVgutter}
/>
</div>
<div style={{ marginBottom: '16px' }}>
<span>Column Count: </span>
<Slider
min={2}
max={12}
step={1}
value={colCount}
onChange={setColCount}
/>
</div>
<Row gutter={[gutter, vgutter]} {...rowProps}>
{cols}
</Row>
</div>
);
},
};
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',
},
},
},
{
component: 'Col',
props: {
span: 4,
children: 'col-4 (tall)',
style: {
background: '#e6f4ff',
padding: '24px 8px',
border: '1px solid #91caff',
textAlign: 'center',
},
},
},
{
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.',
},
liveExample: `function Demo() {
return (
<Row gutter={[16, 16]}>
<Col span={12}>
<div style={{ background: '#e6f4ff', padding: '8px', border: '1px solid #91caff' }}>col-12</div>
</Col>
<Col span={12}>
<div style={{ background: '#e6f4ff', padding: '8px', border: '1px solid #91caff' }}>col-12</div>
</Col>
<Col span={8}>
<div style={{ background: '#e6f4ff', padding: '8px', border: '1px solid #91caff' }}>col-8</div>
</Col>
<Col span={8}>
<div style={{ background: '#e6f4ff', padding: '8px', border: '1px solid #91caff' }}>col-8</div>
</Col>
<Col span={8}>
<div style={{ background: '#e6f4ff', padding: '8px', border: '1px solid #91caff' }}>col-8</div>
</Col>
</Row>
);
}`,
examples: [
{
title: 'Responsive Grid',
code: `function ResponsiveGrid() {
return (
<Row gutter={[16, 16]}>
<Col xs={24} sm={12} md={8} lg={6}>
<div style={{ background: '#e6f4ff', padding: '16px', border: '1px solid #91caff', textAlign: 'center' }}>
Responsive
</div>
</Col>
<Col xs={24} sm={12} md={8} lg={6}>
<div style={{ background: '#e6f4ff', padding: '16px', border: '1px solid #91caff', textAlign: 'center' }}>
Responsive
</div>
</Col>
<Col xs={24} sm={12} md={8} lg={6}>
<div style={{ background: '#e6f4ff', padding: '16px', border: '1px solid #91caff', textAlign: 'center' }}>
Responsive
</div>
</Col>
<Col xs={24} sm={12} md={8} lg={6}>
<div style={{ background: '#e6f4ff', padding: '16px', border: '1px solid #91caff', textAlign: 'center' }}>
Responsive
</div>
</Col>
</Row>
);
}`,
},
{
title: 'Alignment',
code: `function AlignmentDemo() {
const boxStyle = { background: '#e6f4ff', padding: '16px 0', border: '1px solid #91caff', textAlign: 'center' };
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<Row justify="start" gutter={8}>
<Col span={4}><div style={boxStyle}>start</div></Col>
<Col span={4}><div style={boxStyle}>start</div></Col>
</Row>
<Row justify="center" gutter={8}>
<Col span={4}><div style={boxStyle}>center</div></Col>
<Col span={4}><div style={boxStyle}>center</div></Col>
</Row>
<Row justify="end" gutter={8}>
<Col span={4}><div style={boxStyle}>end</div></Col>
<Col span={4}><div style={boxStyle}>end</div></Col>
</Row>
<Row justify="space-between" gutter={8}>
<Col span={4}><div style={boxStyle}>between</div></Col>
<Col span={4}><div style={boxStyle}>between</div></Col>
</Row>
</div>
);
}`,
},
],
},
};
// Keep original for backwards compatibility
export const GridStory: Story = {
render: ({ align, justify, wrap, ...rest }: RowProps & ColProps) => {
const [gutter, setGutter] = useState(24);

View File

@@ -22,38 +22,6 @@ import { IconButton } from '.';
export default {
title: 'Components/IconButton',
component: IconButton,
argTypes: {
altText: {
control: 'text',
description:
'The alt text for the button. If not provided, the button text is used as the alt text by default.',
table: {
type: { summary: 'string' },
},
},
buttonText: {
control: 'text',
description: 'The text inside the button',
table: {
type: { summary: 'string' },
},
},
icon: {
control: false,
description: 'Icon inside the button',
table: {
type: { summary: 'string' },
defaultValue: { summary: 'string' },
},
},
padded: {
control: 'boolean',
description: 'add padding between icon and button text',
table: {
type: { summary: 'boolean' },
},
},
},
parameters: {
docs: {
description: {
@@ -69,6 +37,63 @@ export default {
type Story = StoryObj<typeof IconButton>;
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',
description:
'The alt text for the button. If not provided, the button text is used as the alt text by default.',
table: {
type: { summary: 'string' },
},
},
buttonText: {
control: 'text',
description: 'The text inside the button.',
table: {
type: { summary: 'string' },
},
},
icon: {
control: 'text',
description: 'Icon inside the button (URL or path).',
table: {
type: { summary: 'string' },
},
},
padded: {
control: 'boolean',
description: 'Add padding between icon and button text.',
table: {
type: { summary: 'boolean' },
},
},
},
parameters: {
docs: {
description: {
story: 'A button with an icon and text label.',
},
liveExample: `function Demo() {
return (
<IconButton
buttonText="Superset"
icon="https://superset.apache.org/img/superset-logo-horiz.svg"
padded
onClick={() => alert('Clicked!')}
/>
);
}`,
},
},
};
export const Default: Story = {
args: {
buttonText: 'Default IconButton',
@@ -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',
},
};

View File

@@ -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 (
<IconTooltip tooltip="Helpful information">
<Icons.InfoCircleOutlined iconSize="l" />
</IconTooltip>
);
}`,
},
};

View File

@@ -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<string, string | null> = {
@@ -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 (
<div style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
<Icons.InfoCircleOutlined iconSize="xl" />
<Icons.CheckCircleOutlined iconSize="xl" />
<Icons.WarningOutlined iconSize="xl" />
<Icons.CloseCircleOutlined iconSize="xl" />
</div>
);
}`,
examples: [
{
title: 'Icon Sizes',
code: `function IconSizes() {
const sizes = ['s', 'm', 'l', 'xl', 'xxl'];
return (
<div style={{ display: 'flex', gap: 24, alignItems: 'end' }}>
{sizes.map(size => (
<div key={size} style={{ textAlign: 'center' }}>
<Icons.DatabaseOutlined iconSize={size} />
<div style={{ fontSize: 12, marginTop: 8, color: '#666' }}>{size}</div>
</div>
))}
</div>
);
}`,
},
{
title: 'Icon Gallery',
code: `function IconGallery() {
const Section = ({ title, children }) => (
<div style={{ marginBottom: 24 }}>
<div style={{ fontWeight: 600, marginBottom: 8, color: '#666' }}>{title}</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 16 }}>{children}</div>
</div>
);
return (
<div>
<Section title="Charts">
<Icons.LineChartOutlined iconSize="xl" />
<Icons.BarChartOutlined iconSize="xl" />
<Icons.PieChartOutlined iconSize="xl" />
<Icons.AreaChartOutlined iconSize="xl" />
<Icons.DashboardOutlined iconSize="xl" />
<Icons.FundProjectionScreenOutlined iconSize="xl" />
</Section>
<Section title="Data">
<Icons.DatabaseOutlined iconSize="xl" />
<Icons.TableOutlined iconSize="xl" />
<Icons.ConsoleSqlOutlined iconSize="xl" />
<Icons.FilterOutlined iconSize="xl" />
<Icons.FieldNumberOutlined iconSize="xl" />
<Icons.FieldTimeOutlined iconSize="xl" />
<Icons.FunctionOutlined iconSize="xl" />
<Icons.CalculatorOutlined iconSize="xl" />
</Section>
<Section title="Actions">
<Icons.PlusOutlined iconSize="xl" />
<Icons.EditOutlined iconSize="xl" />
<Icons.DeleteOutlined iconSize="xl" />
<Icons.CopyOutlined iconSize="xl" />
<Icons.SaveOutlined iconSize="xl" />
<Icons.DownloadOutlined iconSize="xl" />
<Icons.UploadOutlined iconSize="xl" />
<Icons.ReloadOutlined iconSize="xl" />
<Icons.SyncOutlined iconSize="xl" />
<Icons.SearchOutlined iconSize="xl" />
<Icons.ExpandOutlined iconSize="xl" />
<Icons.FullscreenOutlined iconSize="xl" />
<Icons.ShareAltOutlined iconSize="xl" />
<Icons.ExportOutlined iconSize="xl" />
</Section>
<Section title="Status">
<Icons.CheckOutlined iconSize="xl" />
<Icons.CheckCircleOutlined iconSize="xl" />
<Icons.CloseOutlined iconSize="xl" />
<Icons.CloseCircleOutlined iconSize="xl" />
<Icons.InfoCircleOutlined iconSize="xl" />
<Icons.WarningOutlined iconSize="xl" />
<Icons.ExclamationCircleOutlined iconSize="xl" />
<Icons.QuestionCircleOutlined iconSize="xl" />
<Icons.LoadingOutlined iconSize="xl" />
<Icons.StopOutlined iconSize="xl" />
</Section>
<Section title="Navigation">
<Icons.MenuOutlined iconSize="xl" />
<Icons.DownOutlined iconSize="xl" />
<Icons.UpOutlined iconSize="xl" />
<Icons.RightOutlined iconSize="xl" />
<Icons.CaretDownOutlined iconSize="xl" />
<Icons.CaretUpOutlined iconSize="xl" />
<Icons.ArrowRightOutlined iconSize="xl" />
<Icons.MoreOutlined iconSize="xl" />
<Icons.EllipsisOutlined iconSize="xl" />
</Section>
<Section title="Objects">
<Icons.FileOutlined iconSize="xl" />
<Icons.FileTextOutlined iconSize="xl" />
<Icons.FileImageOutlined iconSize="xl" />
<Icons.BookOutlined iconSize="xl" />
<Icons.TagOutlined iconSize="xl" />
<Icons.TagsOutlined iconSize="xl" />
<Icons.StarOutlined iconSize="xl" />
<Icons.BellOutlined iconSize="xl" />
<Icons.CalendarOutlined iconSize="xl" />
<Icons.ClockCircleOutlined iconSize="xl" />
<Icons.MailOutlined iconSize="xl" />
<Icons.LinkOutlined iconSize="xl" />
<Icons.LockOutlined iconSize="xl" />
<Icons.UnlockOutlined iconSize="xl" />
<Icons.KeyOutlined iconSize="xl" />
</Section>
<Section title="Users">
<Icons.UserOutlined iconSize="xl" />
<Icons.UserAddOutlined iconSize="xl" />
<Icons.UsergroupAddOutlined iconSize="xl" />
<Icons.LoginOutlined iconSize="xl" />
</Section>
<Section title="Settings">
<Icons.SettingOutlined iconSize="xl" />
<Icons.BgColorsOutlined iconSize="xl" />
<Icons.FormatPainterOutlined iconSize="xl" />
<Icons.HighlightOutlined iconSize="xl" />
<Icons.EyeOutlined iconSize="xl" />
<Icons.EyeInvisibleOutlined iconSize="xl" />
<Icons.SunOutlined iconSize="xl" />
<Icons.MoonOutlined iconSize="xl" />
</Section>
</div>
);
}`,
},
{
title: 'Icon with Text',
code: `function IconWithText() {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Icons.CheckCircleOutlined iconSize="l" style={{ color: '#52c41a' }} />
<span>Success message</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Icons.InfoCircleOutlined iconSize="l" style={{ color: '#1890ff' }} />
<span>Information message</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Icons.WarningOutlined iconSize="l" style={{ color: '#faad14' }} />
<span>Warning message</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Icons.CloseCircleOutlined iconSize="l" style={{ color: '#ff4d4f' }} />
<span>Error message</span>
</div>
</div>
);
}`,
},
],
},
};

View File

@@ -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<typeof Input> = {
export default {
title: 'Components/Input',
component: Input,
};
export default meta;
type Story = StoryObj<typeof Input>;
type InputNumberStory = StoryObj<typeof InputNumber>;
type TextAreaStory = StoryObj<typeof Input.TextArea>;
@@ -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',

View File

@@ -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<InteractiveLabelProps> = args => (
<Label {...args}>{args.children}</Label>
);
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' },
},
};

View File

@@ -28,88 +28,6 @@ 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' },
},
},
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' },
},
},
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' },
},
},
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,
},
},
reverseArrow: {
control: 'boolean',
description: 'Whether the arrow icon is reversed',
table: {
category: 'Sider',
type: { summary: 'boolean' },
},
},
theme: {
control: 'select',
options: ['light', 'dark'],
description: 'Theme for the Sider',
table: {
category: 'Sider',
type: { summary: 'string' },
defaultValue: { summary: 'dark' },
},
},
width: {
control: 'number',
description: 'Width of the Sider',
table: {
category: 'Sider',
type: { summary: 'number' },
defaultValue: { summary: '200' },
},
},
},
parameters: {
docs: {
description: {
@@ -122,6 +40,178 @@ export default {
type Story = StoryObj<typeof Layout>;
export const InteractiveLayout: Story = {
args: {
hasSider: false,
},
argTypes: {
hasSider: {
control: 'boolean',
description: 'Whether the layout contains a Sider sub-component.',
},
},
render: ({
className,
hasSider,
...siderProps
}: LayoutProps & SiderProps) => (
<Layout
className={className}
hasSider={hasSider}
style={{ minHeight: '400px' }}
>
{hasSider && (
<Sider {...siderProps}>
<div
className="logo"
style={{
height: '32px',
margin: '16px',
background: '#ffffff30',
}}
/>
<Menu defaultSelectedKeys={['1']} mode="inline">
<Menu.Item key="1" icon={<Icons.MenuUnfoldOutlined />}>
Option 1
</Menu.Item>
<Menu.Item key="2" icon={<Icons.MenuFoldOutlined />}>
Option 2
</Menu.Item>
</Menu>
</Sider>
)}
<Layout>
<Header
style={{
background: '#fff',
padding: '0 16px',
textAlign: 'center',
}}
>
Header
</Header>
<Content
style={{
margin: '16px',
padding: '24px',
background: '#fff',
textAlign: 'center',
}}
>
Content Area
</Content>
<Footer style={{ textAlign: 'center' }}>
Ant Design Layout Footer
</Footer>
</Layout>
</Layout>
),
};
InteractiveLayout.parameters = {
docs: {
staticProps: {
style: { minHeight: 200 },
},
sampleChildren: [
{
component: 'Layout.Header',
props: {
children: 'Header',
style: {
background: '#001529',
color: '#fff',
padding: '0 24px',
lineHeight: '64px',
},
},
},
{
component: 'Layout.Content',
props: {
children: 'Content Area',
style: { padding: '24px', background: '#fff', flex: 1 },
},
},
{
component: 'Layout.Footer',
props: {
children: 'Footer',
style: {
textAlign: 'center',
background: '#f5f5f5',
padding: '12px',
},
},
},
],
description: {
story: 'Layout component with Header, Footer, Sider, and Content areas.',
},
liveExample: `function Demo() {
return (
<Layout style={{ minHeight: '300px' }}>
<Layout.Sider theme="dark" width={200}>
<div style={{ color: '#fff', padding: '16px' }}>Sidebar</div>
</Layout.Sider>
<Layout>
<Layout.Header style={{ background: '#fff', padding: '0 16px' }}>
Header
</Layout.Header>
<Layout.Content style={{ margin: '16px', padding: '24px', background: '#fff' }}>
Content
</Layout.Content>
<Layout.Footer style={{ textAlign: 'center' }}>
Footer
</Layout.Footer>
</Layout>
</Layout>
);
}`,
examples: [
{
title: 'Content Only',
code: `function ContentOnly() {
return (
<Layout>
<Layout.Header style={{ background: '#001529', color: '#fff', padding: '0 24px', lineHeight: '64px' }}>
Application Header
</Layout.Header>
<Layout.Content style={{ padding: '24px', minHeight: '200px', background: '#fff' }}>
Main content area without a sidebar
</Layout.Content>
<Layout.Footer style={{ textAlign: 'center', background: '#f5f5f5' }}>
Footer Content
</Layout.Footer>
</Layout>
);
}`,
},
{
title: 'Right Sidebar',
code: `function RightSidebar() {
return (
<Layout style={{ minHeight: '300px' }}>
<Layout>
<Layout.Header style={{ background: '#fff', padding: '0 24px' }}>
Header
</Layout.Header>
<Layout.Content style={{ padding: '24px', background: '#fff' }}>
Content with right sidebar
</Layout.Content>
</Layout>
<Layout.Sider theme="light" width={200} style={{ background: '#fafafa' }}>
<div style={{ padding: '16px' }}>Right Sidebar</div>
</Layout.Sider>
</Layout>
);
}`,
},
],
},
};
// Keep original for backwards compatibility
export const LayoutStory: Story = {
render: ({
className,

View File

@@ -38,7 +38,6 @@ export const InteractiveList = (args: ListProps<any>) => (
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 (
<List
bordered
dataSource={data}
renderItem={(item) => <List.Item>{item}</List.Item>}
/>
);
}`,
},
};

View File

@@ -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<typeof ListViewCard>;
type Story = StoryObj<typeof ListViewCard>;
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 (
<ListViewCard
title="My Dashboard"
description="A sample dashboard card"
url="/dashboard/1"
imgURL="https://picsum.photos/seed/demo/300/200"
coverLeft="Created: Jan 2024"
coverRight="Views: 1,234"
/>
);
}`,
},
},
};
export const SupersetListViewCard = ({
loading = false,
}: {
loading?: boolean;
}) => (
<ListViewCard
title="Superset Card Title"
loading={loading}
url="/superset/dashboard/births/"
imgURL="https://images.unsplash.com/photo-1658163724548-29ef00812a54?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2670&q=80"
imgFallbackURL="https://images.unsplash.com/photo-1658208193219-e859d9771912?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2670&q=80"
description="Lorem ipsum dolor sit amet, consectetur adipiscing elit..."
coverLeft="Left Section"
coverRight="Right Section"
/>
);
// 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',
},
};

View File

@@ -223,25 +223,120 @@ ContextualExamples.parameters = {
export const InteractiveLoading = (args: LoadingProps) => <Loading {...args} />;
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 (
<div>
{['normal', 'floating', 'inline'].map(position => (
<div
key={position}
style={{
marginBottom: 40,
padding: 20,
border: '1px solid #eee',
position: 'relative',
minHeight: 80,
}}
>
<h4 style={{ marginTop: 0 }}>{position}</h4>
<Loading position={position} size="m" />
</div>
))}
</div>
);
}`,
examples: [
{
title: 'Size and Opacity Showcase',
code: `function SizeShowcase() {
const sizes = ['s', 'm', 'l'];
return (
<div style={{ padding: 20 }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 20, alignItems: 'center' }}>
<div><strong>Size</strong></div>
<div><strong>Normal</strong></div>
<div><strong>Muted</strong></div>
<div><strong>Usage</strong></div>
{sizes.map(size => (
<React.Fragment key={size}>
<div style={{ fontWeight: 'bold' }}>
{size.toUpperCase()} ({size === 's' ? '40px' : size === 'm' ? '70px' : '100px'})
</div>
<div style={{ textAlign: 'center', padding: 10, border: '1px solid #eee' }}>
<Loading size={size} position="normal" />
</div>
<div style={{ textAlign: 'center', padding: 10, border: '1px solid #eee' }}>
<Loading size={size} muted position="normal" />
</div>
<div style={{ fontSize: 12, color: '#666' }}>
{size === 's' && 'Filter bars, inline'}
{size === 'm' && 'Explore pages'}
{size === 'l' && 'Full page loading'}
</div>
</React.Fragment>
))}
</div>
</div>
);
}`,
},
{
title: 'Contextual Examples',
code: `function ContextualDemo() {
return (
<div style={{ padding: 20 }}>
<h4>Filter Bar (size="s", muted)</h4>
<div style={{ height: 40, backgroundColor: '#f5f5f5', display: 'flex', alignItems: 'center', padding: '0 10px', gap: 10, marginBottom: 30 }}>
<span>Filter 1:</span>
<Loading size="s" muted position="normal" />
<span>Filter 2:</span>
<Loading size="s" muted position="normal" />
</div>
<h4>Dashboard Grid (size="s", muted)</h4>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 10, marginBottom: 30 }}>
{[1, 2, 3].map(i => (
<div key={i} style={{ height: 100, backgroundColor: '#fafafa', border: '1px solid #ddd', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Loading size="s" muted position="normal" />
</div>
))}
</div>
<h4>Main Loading (size="l")</h4>
<div style={{ height: 200, display: 'flex', alignItems: 'center', justifyContent: 'center', border: '2px dashed #ccc' }}>
<Loading size="l" position="normal" />
</div>
</div>
);
}`,
},
],
},
};

View File

@@ -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 (
<Menu
mode="horizontal"
selectable
items={[
{ label: 'Dashboards', key: 'dashboards' },
{ label: 'Charts', key: 'charts' },
{ label: 'Datasets', key: 'datasets' },
{ label: 'SQL Lab', key: 'sqllab' },
]}
/>
);
}`,
examples: [
{
title: 'Vertical Menu',
code: `function VerticalMenu() {
return (
<Menu
mode="vertical"
style={{ width: 200 }}
items={[
{ label: 'Dashboards', key: 'dashboards' },
{ label: 'Charts', key: 'charts' },
{ label: 'Datasets', key: 'datasets' },
{
label: 'Settings',
key: 'settings',
children: [
{ label: 'Profile', key: 'profile' },
{ label: 'Preferences', key: 'preferences' },
],
},
]}
/>
);
}`,
},
{
title: 'Menu with Icons',
code: `function MenuWithIcons() {
return (
<Menu
mode="horizontal"
items={[
{ label: <><Icons.DashboardOutlined /> Dashboards</>, key: 'dashboards' },
{ label: <><Icons.LineChartOutlined /> Charts</>, key: 'charts' },
{ label: <><Icons.DatabaseOutlined /> Datasets</>, key: 'datasets' },
{ label: <><Icons.ConsoleSqlOutlined /> SQL Lab</>, key: 'sqllab' },
]}
/>
);
}`,
},
],
},
};

View File

@@ -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) => (
<MetadataBar {...args} />
);
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 <MetadataBar items={items} />;
}`,
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 <MetadataBar items={items} />;
}`,
},
{
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 <MetadataBar items={items} />;
}`,
},
],
},
};

View File

@@ -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 (
<>
<Button onClick={() => setIsOpen(true)}>Open Modal</Button>
<Modal
show={isOpen}
onHide={() => setIsOpen(false)}
title="Example Modal"
primaryButtonName="Submit"
onHandledPrimaryAction={() => {
alert('Submitted!');
setIsOpen(false);
}}
>
<p>This is the modal content. Click Submit or close the modal.</p>
</Modal>
</>
);
}`,
examples: [
{
title: 'Danger Modal',
code: `function DangerModal() {
const [isOpen, setIsOpen] = React.useState(false);
return (
<>
<Button buttonStyle="danger" onClick={() => setIsOpen(true)}>Delete Item</Button>
<Modal
show={isOpen}
onHide={() => setIsOpen(false)}
title="Confirm Delete"
primaryButtonName="Delete"
primaryButtonStyle="danger"
onHandledPrimaryAction={() => {
alert('Deleted!');
setIsOpen(false);
}}
>
<p>Are you sure you want to delete this item? This action cannot be undone.</p>
</Modal>
</>
);
}`,
},
{
title: 'Confirmation Dialogs',
code: `function ConfirmationDialogs() {
return (
<div style={{ display: 'flex', gap: 8 }}>
<Button onClick={() => Modal.confirm({
title: 'Confirm Action',
content: 'Are you sure you want to proceed?',
okText: 'Yes',
})}>Confirm</Button>
<Button onClick={() => Modal.warning({
title: 'Warning',
content: 'This action may have consequences.',
})}>Warning</Button>
<Button onClick={() => Modal.error({
title: 'Error',
content: 'Something went wrong.',
})}>Error</Button>
</div>
);
}`,
},
],
},
};
export const ModalFunctions = (props: ModalFuncProps) => (
<div>
<Button onClick={() => Modal.error(props)}>Error</Button>

View File

@@ -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 (
<ModalTrigger
isButton
triggerNode={<span>Click to Open</span>}
modalTitle="Example Modal"
modalBody={<p>This is the modal content. You can put any React elements here.</p>}
width="500px"
responsive
/>
);
}`,
examples: [
{
title: 'With Custom Trigger',
code: `function CustomTrigger() {
return (
<ModalTrigger
triggerNode={
<Button buttonStyle="primary">
<Icons.PlusOutlined /> Add New Item
</Button>
}
modalTitle="Add New Item"
modalBody={
<div>
<p>Fill out the form to add a new item.</p>
<Input placeholder="Item name" />
</div>
}
width="400px"
/>
);
}`,
},
{
title: 'Draggable & Resizable',
code: `function DraggableModal() {
return (
<ModalTrigger
isButton
triggerNode={<span>Open Draggable Modal</span>}
modalTitle="Draggable & Resizable"
modalBody={<p>Try dragging the header or resizing from the corners!</p>}
draggable
resizable
width="500px"
/>
);
}`,
},
],
},
};

View File

@@ -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) => (
</Popover>
);
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 (
<Popover
content="Popover sample content"
title="Popover title"
arrow
>
<Button>Hover me</Button>
</Popover>
);
}`,
examples: [
{
title: 'Click Trigger',
code: `function ClickPopover() {
return (
<Popover
content="This popover appears on click."
title="Click Popover"
trigger="click"
>
<Button>Click me</Button>
</Popover>
);
}`,
},
{
title: 'Placements',
code: `function PlacementsDemo() {
return (
<div style={{ display: 'flex', gap: 16, flexWrap: 'wrap', justifyContent: 'center', padding: '60px 0' }}>
{['top', 'right', 'bottom', 'left'].map(placement => (
<Popover
key={placement}
content={\`This popover is placed on the \${placement}\`}
title={placement}
placement={placement}
>
<Button>{placement}</Button>
</Popover>
))}
</div>
);
}`,
},
{
title: 'Rich Content',
code: `function RichPopover() {
return (
<Popover
title="Dashboard Info"
content={
<div>
<p><strong>Created by:</strong> Admin</p>
<p><strong>Last modified:</strong> Jan 2025</p>
<p><strong>Charts:</strong> 12</p>
</div>
}
>
<Button buttonStyle="primary">
<Icons.InfoCircleOutlined /> View Details
</Button>
</Popover>
);
}`,
},
],
},
};

View File

@@ -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) => (
<ProgressBar {...args} type="line" />
<ProgressBar {...args} />
);
export const InteractiveProgressCircle = (args: ProgressBarProps) => (
@@ -35,6 +43,120 @@ export const InteractiveProgressDashboard = (args: ProgressBarProps) => (
<ProgressBar {...args} type="dashboard" />
);
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 (
<ProgressBar
percent={75}
status="normal"
type="line"
showInfo
/>
);
}`,
examples: [
{
title: 'All Progress Types',
code: `function AllTypesDemo() {
return (
<div style={{ display: 'flex', gap: 40, alignItems: 'center' }}>
<div style={{ flex: 1 }}>
<h4>Line</h4>
<ProgressBar percent={75} type="line" />
</div>
<div>
<h4>Circle</h4>
<ProgressBar percent={75} type="circle" />
</div>
<div>
<h4>Dashboard</h4>
<ProgressBar percent={75} type="dashboard" />
</div>
</div>
);
}`,
},
{
title: 'Status Variants',
code: `function StatusDemo() {
const statuses = ['normal', 'success', 'exception', 'active'];
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
{statuses.map(status => (
<div key={status} style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
<span style={{ width: 80 }}>{status}</span>
<ProgressBar percent={75} status={status} type="line" style={{ flex: 1 }} />
</div>
))}
</div>
);
}`,
},
{
title: 'Custom Colors',
code: `function CustomColors() {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<ProgressBar percent={50} strokeColor="#1890ff" />
<ProgressBar percent={70} strokeColor="#52c41a" />
<ProgressBar percent={30} strokeColor="#faad14" trailColor="#f0f0f0" />
<ProgressBar percent={90} strokeColor="#ff4d4f" />
</div>
);
}`,
},
],
},
};
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;

View File

@@ -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<typeof Radio> = {
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 (
<Radio.Group value={value} onChange={e => setValue(e.target.value)}>
<Radio.Button value="line">Line Chart</Radio.Button>
<Radio.Button value="bar">Bar Chart</Radio.Button>
<Radio.Button value="pie">Pie Chart</Radio.Button>
</Radio.Group>
);
}`,
},
{
title: 'Vertical Radio Group',
code: `function VerticalDemo() {
const [value, setValue] = React.useState('option1');
return (
<Radio.Group value={value} onChange={e => setValue(e.target.value)}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
<Radio value="option1">First option</Radio>
<Radio value="option2">Second option</Radio>
<Radio value="option3">Third option</Radio>
</div>
</Radio.Group>
);
}`,
},
],
},
};
export const RadioButtonStory = (args: RadioProps) => (

View File

@@ -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 (
<div
style={{
width: DEFAULT_WIDTH,
}}
>
<Select
{...args}
options={
Array.isArray(options)
? generateOptions(options, optionsCount)
: options
}
mode="multiple"
/>
</div>
);
export const InteractiveSelect = ({
options: argOptions,
...args
}: SelectProps) => (
<div style={{ width: DEFAULT_WIDTH }}>
<Select
ariaLabel="interactive-select"
options={argOptions ?? options}
{...args}
/>
</div>
);
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.
`,
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' },
],
},
ariaLabel: {
description: `It adds the aria-label tag for accessibility standards.
Must be plain English and localized.
`,
},
labelInValue: {
table: {
disable: true,
liveExample: `function Demo() {
return (
<div style={{ width: 300 }}>
<Select
ariaLabel="demo-select"
options={[
{ label: 'Dashboards', value: 'dashboards' },
{ label: 'Charts', value: 'charts' },
{ label: 'Datasets', value: 'datasets' },
{ label: 'SQL Lab', value: 'sqllab' },
{ label: 'Settings', value: 'settings' },
]}
placeholder="Select ..."
showSearch
/>
</div>
);
}`,
examples: [
{
title: 'Multi Select',
code: `function MultiSelectDemo() {
return (
<div style={{ width: 400 }}>
<Select
ariaLabel="multi-select"
mode="multiple"
options={[
{ label: 'Dashboards', value: 'dashboards' },
{ label: 'Charts', value: 'charts' },
{ label: 'Datasets', value: 'datasets' },
{ label: 'SQL Lab', value: 'sqllab' },
{ label: 'Settings', value: 'settings' },
]}
placeholder="Select items..."
allowSelectAll
maxTagCount={3}
/>
</div>
);
}`,
},
},
name: {
table: {
disable: true,
{
title: 'Allow New Options',
code: `function AllowNewDemo() {
return (
<div style={{ width: 300 }}>
<Select
ariaLabel="allow-new-select"
mode="multiple"
options={[
{ label: 'Red', value: 'red' },
{ label: 'Green', value: 'green' },
{ label: 'Blue', value: 'blue' },
]}
placeholder="Type to add tags..."
allowNewOptions
showSearch
/>
</div>
);
}`,
},
},
notFoundContent: {
table: {
disable: true,
{
title: 'Inverted Selection',
code: `function InvertedDemo() {
return (
<div style={{ width: 400 }}>
<Select
ariaLabel="inverted-select"
mode="multiple"
options={[
{ label: 'Admin', value: 'admin' },
{ label: 'Editor', value: 'editor' },
{ label: 'Viewer', value: 'viewer' },
{ label: 'Public', value: 'public' },
]}
placeholder="Exclude roles..."
invertSelection
/>
</div>
);
}`,
},
},
mappedMode: {
table: {
disable: true,
{
title: 'One Line Mode',
code: `function OneLineDemo() {
return (
<div style={{ width: 300 }}>
<Select
ariaLabel="oneline-select"
mode="multiple"
options={[
{ label: 'Dashboard 1', value: 'd1' },
{ label: 'Dashboard 2', value: 'd2' },
{ label: 'Dashboard 3', value: 'd3' },
{ label: 'Dashboard 4', value: 'd4' },
{ label: 'Dashboard 5', value: 'd5' },
]}
placeholder="Select dashboards..."
oneLine
/>
</div>
);
}`,
},
},
mode: {
description: `It defines whether the Select should allow for
the selection of multiple options or single. Single by default.
`,
control: {
type: 'inline-radio',
options: ['single', 'multiple'],
},
},
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',
},
},
header: {
description: `It adds a header on top of the Select. Can be any ReactNode.`,
control: { type: 'inline-radio', options: ['none', 'text', 'control'] },
},
pageSize: {
description: `It defines how many results should be included in the query response.
Works in async mode only (See the options property).
`,
},
fetchOnlyOnSearch: {
description: `It fires a request against the server only after searching.
Works in async mode only (See the options property).
Undefined by default.
`,
},
],
},
};
@@ -310,3 +381,49 @@ PageScroll.parameters = {
disable: true,
},
};
/**
* Extended interactive story for Storybook with additional controls
* (header, optionsCount, generateOptions) that are story-specific utilities.
*/
export const AdvancedPlayground = (
args: SelectProps & { optionsCount: number },
) => {
const { optionsCount, ...selectArgs } = args;
return (
<div style={{ width: DEFAULT_WIDTH }}>
<Select
{...selectArgs}
options={generateOptions(options, optionsCount)}
/>
</div>
);
};
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' },
},
};

View File

@@ -17,115 +17,21 @@
* 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 },
argTypes: {
// Skeleton props
active: {
control: 'boolean',
description: 'Show animation effect',
table: {
category: 'Skeleton',
type: { summary: 'boolean' },
defaultValue: { summary: 'false' },
},
},
avatar: {
control: 'boolean',
description: 'Show avatar placeholder',
table: {
category: 'Skeleton',
type: { summary: 'boolean | object' },
defaultValue: { summary: false },
},
},
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 },
},
},
title: {
control: 'boolean',
description: 'Show title placeholder',
table: {
category: 'Skeleton',
type: { summary: 'boolean | object' },
defaultValue: { summary: true },
},
},
// Skeleton.Avatar props
shape: {
control: 'select',
description: 'Shape of the avatar',
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' },
},
},
// 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 },
},
},
},
parameters: {
docs: {
description: {
@@ -134,14 +40,91 @@ export default {
},
},
},
} as Meta<
typeof Skeleton & typeof Avatar & typeof Button & typeof Input & typeof Image
>;
} as Meta<typeof Skeleton>;
type Story = StoryObj<typeof Skeleton & typeof Button & typeof Avatar>;
type Story = StoryObj<SkeletonStoryArgs>;
export const InteractiveSkeleton: Story = {
args: {
active: true,
avatar: false,
loading: true,
title: true,
shape: 'circle',
size: 'default',
block: false,
},
argTypes: {
active: {
control: 'boolean',
description: 'Show animation effect.',
},
avatar: {
control: 'boolean',
description: 'Show avatar placeholder.',
},
loading: {
control: 'boolean',
description: 'Display the skeleton when true.',
},
title: {
control: 'boolean',
description: 'Show title placeholder.',
},
shape: {
control: 'select',
description: 'Shape of the avatar/button skeleton.',
options: ['circle', 'square'],
},
size: {
control: 'select',
options: ['large', 'small', 'default'],
description: 'Size of the skeleton elements.',
},
block: {
control: 'boolean',
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 (
<Space direction="vertical" size="middle">
Skeleton
<Skeleton {...args} />
Avatar
<Skeleton.Avatar {...avatar} />
Button
<Skeleton.Button {...button} />
</Space>
);
},
parameters: {
docs: {
description: {
story: 'A loading placeholder for content that is not yet loaded.',
},
liveExample: `function Demo() {
return (
<Skeleton active avatar paragraph={{ rows: 4 }} />
);
}`,
},
},
};
// Keep original for backwards compatibility
export const SkeletonStory: Story = {
render: (args: SkeletonProps & AvatarProps & SkeletonButtonProps) => {
render: args => {
const avatar = {
shape: args.shape,
size: args.size,

View File

@@ -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 (
<div style={{ width: 400, padding: '20px 0' }}>
<Slider
min={0}
max={100}
defaultValue={70}
step={1}
/>
</div>
);
}`,
examples: [
{
title: 'Range Slider',
code: `function RangeSliderDemo() {
return (
<div style={{ width: 400, padding: '20px 0' }}>
<h4>Basic Range</h4>
<Slider range defaultValue={[20, 70]} min={0} max={100} />
<br />
<h4>Draggable Track</h4>
<Slider range={{ draggableTrack: true }} defaultValue={[30, 60]} min={0} max={100} />
</div>
);
}`,
},
{
title: 'With Marks',
code: `function MarksDemo() {
return (
<div style={{ width: 400, padding: '20px 0' }}>
<Slider
min={0}
max={100}
defaultValue={37}
marks={{
0: '0°C',
25: '25°C',
50: '50°C',
75: '75°C',
100: '100°C',
}}
/>
</div>
);
}`,
},
{
title: 'Stepped and Dots',
code: `function SteppedDemo() {
return (
<div style={{ width: 400, padding: '20px 0' }}>
<h4>Step = 10 with Dots</h4>
<Slider min={0} max={100} defaultValue={30} step={10} dots />
<br />
<h4>Step = 25</h4>
<Slider min={0} max={100} defaultValue={50} step={25} dots
marks={{ 0: '0', 25: '25', 50: '50', 75: '75', 100: '100' }} />
</div>
);
}`,
},
{
title: 'Vertical Slider',
code: `function VerticalDemo() {
return (
<div style={{ height: 300, display: 'flex', gap: 40, padding: '0 40px' }}>
<Slider vertical defaultValue={30} />
<Slider vertical range defaultValue={[20, 60]} />
<Slider vertical defaultValue={50} dots step={10}
marks={{ 0: '0', 50: '50', 100: '100' }} />
</div>
);
}`,
},
],
},
};
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.',
},
};

View File

@@ -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) => (
<Space {...args}>
{new Array(20).fill(null).map((_, i) => (
<p key={i}>Item</p>
{SAMPLE_ITEMS.map((item, i) => (
<div key={i} style={sampleItemStyle}>
{item}
</div>
))}
</Space>
);
@@ -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 (
<Space size="small">
{['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5'].map(item => (
<div
key={item}
style={{
padding: '8px 16px',
background: '#e6f4ff',
border: '1px solid #91caff',
borderRadius: 4,
}}
>
{item}
</div>
))}
</Space>
);
}`,
examples: [
{
title: 'Vertical Space',
code: `function VerticalSpace() {
return (
<Space direction="vertical" size="middle" style={{ display: 'flex' }}>
<Button buttonStyle="primary">Primary</Button>
<Button buttonStyle="secondary">Secondary</Button>
<Button buttonStyle="dashed">Dashed</Button>
</Space>
);
}`,
},
{
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 (
<div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
{['small', 'middle', 'large'].map(size => (
<div key={size}>
<h4>{size}</h4>
<Space size={size}>
{items.map(item => (
<div key={item} style={itemStyle}>{item}</div>
))}
</Space>
</div>
))}
</div>
);
}`,
},
],
},
};

View File

@@ -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) => <Steps {...args} />;
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 (
<Steps
current={1}
items={[
{ title: 'Connect Database', description: 'Configure the connection' },
{ title: 'Create Dataset', description: 'Select tables and columns' },
{ title: 'Build Chart', description: 'Choose visualization type' },
]}
/>
);
}`,
examples: [
{
title: 'Vertical Steps',
code: `function VerticalSteps() {
return (
<Steps
direction="vertical"
current={1}
items={[
{ title: 'Upload CSV', description: 'Select a file from your computer' },
{ title: 'Configure Columns', description: 'Set data types and names' },
{ title: 'Review', description: 'Verify the data looks correct' },
{ title: 'Import', description: 'Save the dataset' },
]}
/>
);
}`,
},
{
title: 'Status Indicators',
code: `function StatusSteps() {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 32 }}>
<div>
<h4>Error on Step 2</h4>
<Steps
current={1}
status="error"
items={[
{ title: 'Connection', description: 'Configured' },
{ title: 'Validation', description: 'Failed to validate' },
{ title: 'Complete' },
]}
/>
</div>
<div>
<h4>All Complete</h4>
<Steps
current={3}
items={[
{ title: 'Step 1' },
{ title: 'Step 2' },
{ title: 'Step 3' },
]}
/>
</div>
</div>
);
}`,
},
{
title: 'Dot Style and Small Size',
code: `function DotAndSmall() {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 32 }}>
<div>
<h4>Progress Dots</h4>
<Steps
progressDot
current={1}
items={[
{ title: 'Create', description: 'Define the resource' },
{ title: 'Configure', description: 'Set parameters' },
{ title: 'Deploy', description: 'Go live' },
]}
/>
</div>
<div>
<h4>Small Size</h4>
<Steps
size="small"
current={2}
items={[
{ title: 'Login' },
{ title: 'Verify' },
{ title: 'Done' },
]}
/>
</div>
</div>
);
}`,
},
],
},
};

View File

@@ -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 (
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch
checked={checked}
onChange={setChecked}
title="Toggle feature"
/>
<span>{checked ? 'On' : 'Off'}</span>
<span style={{ color: '#999', fontSize: 12 }}>(hover the switch to see the title tooltip)</span>
</div>
);
}`,
examples: [
{
title: 'Switch States',
code: `function SwitchStates() {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch defaultChecked title="Enabled switch" />
<span>Checked</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch title="Unchecked switch" />
<span>Unchecked</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch disabled defaultChecked title="Disabled on" />
<span>Disabled (on)</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch disabled title="Disabled off" />
<span>Disabled (off)</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch loading defaultChecked title="Loading switch" />
<span>Loading</span>
</div>
</div>
);
}`,
},
{
title: 'Sizes',
code: `function SizesDemo() {
return (
<div style={{ display: 'flex', alignItems: 'center', gap: 24 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch size="small" defaultChecked title="Small switch" />
<span>Small</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch size="default" defaultChecked title="Default switch" />
<span>Default</span>
</div>
</div>
);
}`,
},
{
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 (
<div style={{ maxWidth: 320, border: '1px solid #e8e8e8', borderRadius: 8, padding: 16 }}>
<h4 style={{ marginTop: 0 }}>Dashboard Settings</h4>
{[
{ 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 }) => (
<div key={label} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '8px 0', borderBottom: '1px solid #f0f0f0' }}>
<span>{label}</span>
<Switch checked={checked} onChange={onChange} title={title} />
</div>
))}
</div>
);
}`,
},
],
},
};

View File

@@ -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<typeof Table>;
interface BasicData {
@@ -211,6 +218,187 @@ const basicColumns: ColumnsType<BasicData> = [
},
];
// Interactive story for docs generation
export const InteractiveTable = (args: TableProps<object>) => (
<Table {...args} />
);
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 <Table data={data} columns={columns} size="small" />;
}`,
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 (
<Table
data={data}
columns={columns}
size="small"
pageSizeOptions={['5', '10']}
defaultPageSize={5}
/>
);
}`,
},
{
title: 'Loading State',
code: `function LoadingTable() {
const columns = [
{ title: 'Name', dataIndex: 'name', key: 'name' },
{ title: 'Status', dataIndex: 'status', key: 'status' },
];
return <Table data={[]} columns={columns} size="small" loading />;
}`,
},
],
},
};
const bigColumns: ColumnsType<ExampleData> = [
{
title: 'Name',

View File

@@ -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 = () => (

View File

@@ -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 = {
emptyWrapperType: {
control: {
type: 'select',
},
options: [EmptyWrapperType.Default, EmptyWrapperType.Small],
},
pageSize: {
control: {
type: 'number',
min: 1,
},
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' },
options: [EmptyWrapperType.Default, EmptyWrapperType.Small],
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 (
<TableView
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.' },
]}
initialSortBy={[{ id: 'name', desc: true }]}
pageSize={2}
withPagination
showRowCount
/>
);
}`,
examples: [
{
title: 'Without Pagination',
code: `function NoPaginationDemo() {
return (
<TableView
columns={[
{ accessor: 'name', Header: 'Name', id: 'name' },
{ accessor: 'email', Header: 'Email', id: 'email' },
{ accessor: 'status', Header: 'Status', id: 'status' },
]}
data={[
{ name: 'Alice', email: 'alice@example.com', status: 'Active' },
{ name: 'Bob', email: 'bob@example.com', status: 'Inactive' },
{ name: 'Charlie', email: 'charlie@example.com', status: 'Active' },
]}
withPagination={false}
/>
);
}`,
},
{
title: 'Empty State',
code: `function EmptyDemo() {
return (
<TableView
columns={[
{ accessor: 'name', Header: 'Name', id: 'name' },
{ accessor: 'value', Header: 'Value', id: 'value' },
]}
data={[]}
noDataText="No results found"
/>
);
}`,
},
{
title: 'With Sorting',
code: `function SortingDemo() {
return (
<TableView
columns={[
{ accessor: 'id', Header: 'ID', id: 'id', sortable: true },
{ accessor: 'name', Header: 'Name', id: 'name', sortable: true },
{ accessor: 'score', Header: 'Score', id: 'score', sortable: true },
]}
data={[
{ id: 1, name: 'Dashboard A', score: 95 },
{ id: 2, name: 'Dashboard B', score: 72 },
{ id: 3, name: 'Dashboard C', score: 88 },
{ id: 4, name: 'Dashboard D', score: 64 },
]}
initialSortBy={[{ id: 'score', desc: true }]}
withPagination={false}
/>
);
}`,
},
],
},
};

View File

@@ -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) => <Tabs {...args} />;
// 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) => (
<Tabs {...args} items={demoItems} />
);
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 (
<Tabs
defaultActiveKey="1"
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' },
]}
/>
);
}`,
examples: [
{
title: 'Card Style',
code: `function CardTabs() {
return (
<Tabs
type="card"
defaultActiveKey="1"
items={[
{ key: '1', label: 'Dashboards', children: 'View and manage your dashboards.' },
{ key: '2', label: 'Charts', children: 'Browse all saved charts.' },
{ key: '3', label: 'Datasets', children: 'Explore available datasets.' },
]}
/>
);
}`,
},
{
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 (
<div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
{['top', 'bottom', 'left', 'right'].map(pos => (
<div key={pos}>
<h4>{pos}</h4>
<Tabs tabPosition={pos} defaultActiveKey="1" items={items} />
</div>
))}
</div>
);
}`,
},
{
title: 'With Icons',
code: `function IconTabs() {
return (
<Tabs
defaultActiveKey="1"
items={[
{ key: '1', label: <><Icons.DashboardOutlined /> Dashboards</>, children: 'Dashboard content here.' },
{ key: '2', label: <><Icons.LineChartOutlined /> Charts</>, children: 'Chart content here.' },
{ key: '3', label: <><Icons.DatabaseOutlined /> Datasets</>, children: 'Dataset content here.' },
{ key: '4', label: <><Icons.ConsoleSqlOutlined /> SQL Lab</>, children: 'SQL Lab content here.' },
]}
/>
);
}`,
},
],
},
};

View File

@@ -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) => <Timer {...args} />;
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 (
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
<Timer
startTime={startTime}
isRunning={isRunning}
status="success"
/>
<Button onClick={() => setIsRunning(r => !r)}>
{isRunning ? 'Stop' : 'Start'}
</Button>
</div>
);
}`,
examples: [
{
title: 'Status Variants',
code: `function StatusVariants() {
const [startTime] = React.useState(Date.now());
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
{['success', 'warning', 'danger', 'info', 'default', 'primary', 'secondary'].map(status => (
<div key={status} style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<span style={{ width: 80 }}>{status}</span>
<Timer startTime={startTime} isRunning status={status} />
</div>
))}
</div>
);
}`,
},
{
title: 'Completed Timer',
code: `function CompletedTimer() {
const start = Date.now() - 5230;
const end = Date.now();
return (
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<Timer
startTime={start}
endTime={end}
isRunning={false}
status="success"
/>
<span style={{ color: '#999' }}>Query completed in ~5.2 seconds</span>
</div>
);
}`,
},
{
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 (
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
<Timer
startTime={startTime}
isRunning={isRunning}
status={isRunning ? 'warning' : 'success'}
/>
<Button onClick={handleToggle}>
{isRunning ? 'Pause' : startTime ? 'Resume' : 'Start'}
</Button>
</div>
);
}`,
},
],
},
};

View File

@@ -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) => (
<Tooltip {...args}>
<Button style={{ margin: '50px 100px' }}>Hover me</Button>
@@ -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 (
<Tooltip title="This is a helpful tooltip">
<Button>Hover me</Button>
</Tooltip>
);
}`,
examples: [
{
title: 'Placements',
code: `function Placements() {
const placements = ['top', 'bottom', 'left', 'right', 'topLeft', 'topRight', 'bottomLeft', 'bottomRight'];
return (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
{placements.map(placement => (
<Tooltip key={placement} title={placement} placement={placement}>
<Button>{placement}</Button>
</Tooltip>
))}
</div>
);
}`,
},
{
title: 'Trigger Types',
code: `function Triggers() {
return (
<div style={{ display: 'flex', gap: 12 }}>
<Tooltip title="Hover trigger" trigger="hover">
<Button>Hover</Button>
</Tooltip>
<Tooltip title="Click trigger" trigger="click">
<Button>Click</Button>
</Tooltip>
<Tooltip title="Focus trigger" trigger="focus">
<Button>Focus</Button>
</Tooltip>
</div>
);
}`,
},
],
},
};

View File

@@ -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<typeof Tree>;
export default meta;
};
const treeData: TreeDataNode[] = [
{
@@ -237,3 +98,174 @@ export const TreeStory: Story = {
},
render: (args: TreeProps) => <Tree {...args} />,
};
// Interactive story with primitive args for documentation
export const InteractiveTree = (args: TreeProps) => (
<Tree {...args} treeData={treeData} />
);
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 <Tree treeData={treeData} defaultExpandAll />;
}`,
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 (
<Tree
treeData={treeData}
checkable
defaultExpandAll
checkedKeys={checkedKeys}
onCheck={setCheckedKeys}
/>
);
}`,
},
{
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 <Tree treeData={treeData} showLine showIcon defaultExpandAll />;
}`,
},
],
},
};

View File

@@ -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) => (
<div style={{ width: 300 }}>
<TreeSelect {...args} treeData={treeData} style={{ width: '100%' }} />
</div>
);
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 (
<TreeSelect
style={{ width: 300 }}
value={value}
onChange={setValue}
treeData={treeData}
placeholder="Select an item"
allowClear
treeDefaultExpandAll
/>
);
}`,
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 (
<TreeSelect
style={{ width: 300 }}
value={value}
onChange={setValue}
treeData={treeData}
treeCheckable
placeholder="Select data sources"
treeDefaultExpandAll
allowClear
/>
);
}`,
},
{
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 (
<TreeSelect
style={{ width: 300 }}
treeData={treeData}
treeLine
treeDefaultExpandAll
placeholder="Browse items"
/>
);
}`,
},
],
},
};
// Keep original for backwards compatibility
export const Default: Story = {
args: {
treeData,

View File

@@ -28,129 +28,6 @@ export default {
Paragraph: Typography.Paragraph,
Link: Typography.Link,
},
argTypes: {
code: {
control: 'boolean',
description: 'Code style',
table: {
category: 'Typography.Text',
type: { summary: 'boolean' },
defaultValue: { summary: 'false' },
},
},
copyable: {
control: 'boolean',
description: 'Whether to be copyable, customize it via setting an object',
table: {
category: 'Typography.Text',
type: { summary: 'boolean' },
defaultValue: { summary: 'false' },
},
},
delete: {
control: 'boolean',
description: 'Deleted line style',
table: {
category: 'Typography.Text',
type: { summary: 'boolean' },
defaultValue: { summary: 'false' },
},
},
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' },
},
},
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' },
},
},
keyboard: {
control: 'boolean',
description: 'Keyboard style',
table: {
category: 'Typography.Text',
type: { summary: 'boolean' },
defaultValue: { summary: 'false' },
},
},
mark: {
control: 'boolean',
description: 'Marked style',
table: {
category: 'Typography.Text',
type: { summary: 'boolean' },
defaultValue: { summary: 'false' },
},
},
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' },
},
},
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' },
},
},
strong: {
control: 'boolean',
description: 'Bold style',
table: {
category: 'Typography.Paragraph',
type: { summary: 'boolean' },
defaultValue: { summary: 'false' },
},
},
},
parameters: {
docs: {
description: {
@@ -168,6 +45,154 @@ export default {
type TextStory = StoryObj<typeof Typography.Text>;
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.',
},
copyable: {
control: 'boolean',
description: 'Whether the text is copyable.',
},
delete: {
control: 'boolean',
description: 'Deleted line style.',
},
disabled: {
control: 'boolean',
description: 'Disabled content.',
},
ellipsis: {
control: 'boolean',
description: 'Display ellipsis when text overflows.',
},
keyboard: {
control: 'boolean',
description: 'Keyboard style.',
},
mark: {
control: 'boolean',
description: 'Marked/highlighted style.',
},
italic: {
control: 'boolean',
description: 'Italic style.',
},
underline: {
control: 'boolean',
description: 'Underlined style.',
},
strong: {
control: 'boolean',
description: 'Bold style.',
},
type: {
control: 'select',
options: [undefined, 'secondary', 'success', 'warning', 'danger'],
description: 'Text type for semantic coloring.',
},
},
render: args => <Typography.Text {...args} />,
};
InteractiveTypography.parameters = {
docs: {
renderComponent: 'Typography.Text',
description: {
story: 'Text component with various styling options.',
},
liveExample: `function Demo() {
return (
<div>
<Typography.Text>Default Text</Typography.Text>
<br />
<Typography.Text type="secondary">Secondary</Typography.Text>
<br />
<Typography.Text type="success">Success</Typography.Text>
<br />
<Typography.Text type="warning">Warning</Typography.Text>
<br />
<Typography.Text type="danger">Danger</Typography.Text>
<br />
<Typography.Text code>Code</Typography.Text>
<br />
<Typography.Text keyboard>Keyboard</Typography.Text>
<br />
<Typography.Text mark>Marked</Typography.Text>
<br />
<Typography.Text underline>Underline</Typography.Text>
<br />
<Typography.Text delete>Deleted</Typography.Text>
<br />
<Typography.Text strong>Strong</Typography.Text>
<br />
<Typography.Text italic>Italic</Typography.Text>
</div>
);
}`,
examples: [
{
title: 'All Subcomponents',
code: `function AllSubcomponents() {
return (
<div>
<Typography.Title level={2}>Typography Components</Typography.Title>
<Typography.Paragraph>
The Typography component includes several subcomponents for different text needs.
Use <Typography.Text strong>Title</Typography.Text> for headings,
<Typography.Text code>Text</Typography.Text> for inline text styling,
and <Typography.Text mark>Paragraph</Typography.Text> for block content.
</Typography.Paragraph>
<Typography.Link href="https://superset.apache.org" target="_blank">
Learn more about Apache Superset
</Typography.Link>
</div>
);
}`,
},
{
title: 'Text Styling Options',
code: `function TextStyles() {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
<Typography.Text code>Code style</Typography.Text>
<Typography.Text keyboard>Keyboard style</Typography.Text>
<Typography.Text mark>Highlighted text</Typography.Text>
<Typography.Text underline>Underlined text</Typography.Text>
<Typography.Text delete>Deleted text</Typography.Text>
<Typography.Text strong>Bold text</Typography.Text>
<Typography.Text italic>Italic text</Typography.Text>
<Typography.Text type="success">Success type</Typography.Text>
<Typography.Text type="warning">Warning type</Typography.Text>
<Typography.Text type="danger">Danger type</Typography.Text>
</div>
);
}`,
},
],
},
};
// Keep original for backwards compatibility
export const TextStory: TextStory = {
args: {
children: 'Default Text',
@@ -177,14 +202,169 @@ export const TextStory: TextStory = {
type TitleStory = StoryObj<typeof Typography.Title>;
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 => <Typography.Title {...args} />,
parameters: {
docs: {
description: {
story: 'Title component with heading levels h1-h5.',
},
liveExample: `function Demo() {
return (
<div>
<Typography.Title level={1}>h1. Heading</Typography.Title>
<Typography.Title level={2}>h2. Heading</Typography.Title>
<Typography.Title level={3}>h3. Heading</Typography.Title>
<Typography.Title level={4}>h4. Heading</Typography.Title>
<Typography.Title level={5}>h5. Heading</Typography.Title>
</div>
);
}`,
},
},
};
export const TitleStory: TitleStory = {
args: {
children: 'Default Title',
},
render: args => <Typography.Title {...args} />,
};
type ParagraphStory = StoryObj<typeof Typography.Paragraph>;
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 => <Typography.Paragraph {...args} />,
parameters: {
docs: {
description: {
story: 'Paragraph component for block-level text content.',
},
liveExample: `function Demo() {
return (
<Typography.Paragraph>
This is a paragraph. Paragraphs are used for block-level text content.
They support features like <Typography.Text strong>bold</Typography.Text>,
<Typography.Text italic> italic</Typography.Text>, and
<Typography.Text code> code</Typography.Text> styling.
</Typography.Paragraph>
);
}`,
},
},
};
export const ParagraphStory: ParagraphStory = {
args: {
children: 'Default Paragraph',
@@ -194,6 +374,92 @@ export const ParagraphStory: ParagraphStory = {
type LinkStory = StoryObj<typeof Typography.Link>;
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 => <Typography.Link {...args} />,
parameters: {
docs: {
description: {
story: 'Link component for hyperlinks with text styling options.',
},
liveExample: `function Demo() {
return (
<div>
<Typography.Link href="https://superset.apache.org" target="_blank">
Apache Superset
</Typography.Link>
</div>
);
}`,
},
},
};
export const LinkStory: LinkStory = {
args: {
children: 'Default Link',

View File

@@ -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 (
<div>
<Button onClick={() => setShow(true)}>
Navigate Away (Unsaved Changes)
</Button>
<UnsavedChangesModal
showModal={show}
onHide={() => setShow(false)}
handleSave={() => { alert('Saved!'); setShow(false); }}
onConfirmNavigation={() => { alert('Discarded changes'); setShow(false); }}
title="Unsaved Changes"
>
If you don&apos;t save, changes will be lost.
</UnsavedChangesModal>
</div>
);
}`,
examples: [
{
title: 'Custom Title',
code: `function CustomTitle() {
const [show, setShow] = React.useState(false);
return (
<div>
<Button onClick={() => setShow(true)}>
Close Without Saving
</Button>
<UnsavedChangesModal
showModal={show}
onHide={() => 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?
</UnsavedChangesModal>
</div>
);
}`,
},
],
},
};

View File

@@ -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<typeof Upload> = {
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<typeof Upload>;
export const Default: Story = {
@@ -130,3 +51,74 @@ export const Default: Story = {
</Upload>
),
};
export const InteractiveUpload = (args: any) => <Upload {...args} />;
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 (
<Upload>
<Button>Click to Upload</Button>
</Upload>
);
}`,
examples: [
{
title: 'Picture Card Style',
code: `function PictureCard() {
return (
<Upload listType="picture-card">
+ Upload
</Upload>
);
}`,
},
{
title: 'Drag and Drop',
code: `function DragDrop() {
return (
<Upload.Dragger>
<p style={{ fontSize: 48, color: '#999', margin: 0 }}>+</p>
<p>Click or drag file to this area to upload</p>
<p style={{ color: '#999' }}>Support for single or bulk upload.</p>
</Upload.Dragger>
);
}`,
},
],
},
};

View File

@@ -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 {

View File

@@ -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/<package>/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 <package>`.
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: <string>,
storyName: <string>,
renderStory: <func> () => 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.