mirror of
https://github.com/apache/superset.git
synced 2026-05-29 11:45:16 +00:00
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:
@@ -75,6 +75,7 @@ logos/*
|
||||
erd.puml
|
||||
erd.svg
|
||||
intro_header.txt
|
||||
TODO.md
|
||||
|
||||
# for LLMs
|
||||
llm-context.md
|
||||
|
||||
24
AGENTS.md
24
AGENTS.md
@@ -101,6 +101,30 @@ superset/
|
||||
- **UPDATING.md**: Add breaking changes here
|
||||
- **Docstrings**: Required for new functions/classes
|
||||
|
||||
## Developer Portal: Storybook-to-MDX Documentation
|
||||
|
||||
The Developer Portal auto-generates MDX documentation from Storybook stories. **Stories are the single source of truth.**
|
||||
|
||||
### Core Philosophy
|
||||
- **Fix issues in the STORY, not the generator** - When something doesn't render correctly, update the story file first
|
||||
- **Generator should be lightweight** - It extracts and passes through data; avoid special cases
|
||||
- **Stories define everything** - Props, controls, galleries, examples all come from story metadata
|
||||
|
||||
### Story Requirements for Docs Generation
|
||||
- Use `export default { title: '...' }` (inline), not `const meta = ...; export default meta;`
|
||||
- Name interactive stories `Interactive${ComponentName}` (e.g., `InteractiveButton`)
|
||||
- Define `args` for default prop values
|
||||
- Define `argTypes` at the story level (not meta level) with control types and descriptions
|
||||
- Use `parameters.docs.gallery` for size×style variant grids
|
||||
- Use `parameters.docs.sampleChildren` for components that need children
|
||||
- Use `parameters.docs.liveExample` for custom live code blocks
|
||||
- Use `parameters.docs.staticProps` for complex object props that can't be parsed inline
|
||||
|
||||
### Generator Location
|
||||
- Script: `docs/scripts/generate-superset-components.mjs`
|
||||
- Wrapper: `docs/src/components/StorybookWrapper.jsx`
|
||||
- Output: `docs/developer_portal/components/`
|
||||
|
||||
## Architecture Patterns
|
||||
|
||||
### Security & Features
|
||||
|
||||
115
docs/.claude/instructions.md
Normal file
115
docs/.claude/instructions.md
Normal 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
7
docs/.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -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'],
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.*
|
||||
@@ -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).
|
||||
@@ -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',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
114
docs/developer_portal/testing/storybook.md
Normal file
114
docs/developer_portal/testing/storybook.md
Normal 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
|
||||
@@ -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',
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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",
|
||||
|
||||
1415
docs/scripts/generate-superset-components.mjs
Normal file
1415
docs/scripts/generate-superset-components.mjs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 "{String(component)}" 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 "{String(componentToRender)}" 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 "{String(component)}" 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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -218,7 +218,7 @@ const Community = () => {
|
||||
)}
|
||||
</ul>
|
||||
</StyledJoinCommunity>
|
||||
<BlurredSection>
|
||||
<BlurredSection id="superset-community-calendar">
|
||||
<SectionHeader
|
||||
level="h2"
|
||||
title="Superset Community Calendar"
|
||||
|
||||
118
docs/src/shims/null-module.js
Normal file
118
docs/src/shims/null-module.js
Normal 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
54
docs/src/shims/react-table.js
vendored
Normal 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;
|
||||
@@ -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
10
docs/src/theme.d.ts
vendored
@@ -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;
|
||||
}
|
||||
|
||||
107
docs/src/theme/Playground/Preview/index.tsx
Normal file
107
docs/src/theme/Playground/Preview/index.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
23
docs/src/theme/Playground/Preview/styles.module.css
Normal file
23
docs/src/theme/Playground/Preview/styles.module.css
Normal 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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
BIN
docs/static/img/atomic-design.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 159 KiB |
172
docs/yarn.lock
172
docs/yarn.lock
@@ -312,6 +312,27 @@
|
||||
json5 "^2.2.3"
|
||||
semver "^6.3.1"
|
||||
|
||||
"@babel/core@^7.26.0":
|
||||
version "7.28.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.28.5.tgz#4c81b35e51e1b734f510c99b07dfbc7bbbb48f7e"
|
||||
integrity sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.27.1"
|
||||
"@babel/generator" "^7.28.5"
|
||||
"@babel/helper-compilation-targets" "^7.27.2"
|
||||
"@babel/helper-module-transforms" "^7.28.3"
|
||||
"@babel/helpers" "^7.28.4"
|
||||
"@babel/parser" "^7.28.5"
|
||||
"@babel/template" "^7.27.2"
|
||||
"@babel/traverse" "^7.28.5"
|
||||
"@babel/types" "^7.28.5"
|
||||
"@jridgewell/remapping" "^2.3.5"
|
||||
convert-source-map "^2.0.0"
|
||||
debug "^4.1.0"
|
||||
gensync "^1.0.0-beta.2"
|
||||
json5 "^2.2.3"
|
||||
semver "^6.3.1"
|
||||
|
||||
"@babel/generator@^7.25.9", "@babel/generator@^7.28.3":
|
||||
version "7.28.3"
|
||||
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.28.3.tgz#9626c1741c650cbac39121694a0f2d7451b8ef3e"
|
||||
@@ -323,6 +344,17 @@
|
||||
"@jridgewell/trace-mapping" "^0.3.28"
|
||||
jsesc "^3.0.2"
|
||||
|
||||
"@babel/generator@^7.28.5":
|
||||
version "7.28.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.28.5.tgz#712722d5e50f44d07bc7ac9fe84438742dd61298"
|
||||
integrity sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==
|
||||
dependencies:
|
||||
"@babel/parser" "^7.28.5"
|
||||
"@babel/types" "^7.28.5"
|
||||
"@jridgewell/gen-mapping" "^0.3.12"
|
||||
"@jridgewell/trace-mapping" "^0.3.28"
|
||||
jsesc "^3.0.2"
|
||||
|
||||
"@babel/helper-annotate-as-pure@^7.27.1", "@babel/helper-annotate-as-pure@^7.27.3":
|
||||
version "7.27.3"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz#f31fd86b915fc4daf1f3ac6976c59be7084ed9c5"
|
||||
@@ -354,6 +386,19 @@
|
||||
"@babel/traverse" "^7.28.3"
|
||||
semver "^6.3.1"
|
||||
|
||||
"@babel/helper-create-class-features-plugin@^7.28.5":
|
||||
version "7.28.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.5.tgz#472d0c28028850968979ad89f173594a6995da46"
|
||||
integrity sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ==
|
||||
dependencies:
|
||||
"@babel/helper-annotate-as-pure" "^7.27.3"
|
||||
"@babel/helper-member-expression-to-functions" "^7.28.5"
|
||||
"@babel/helper-optimise-call-expression" "^7.27.1"
|
||||
"@babel/helper-replace-supers" "^7.27.1"
|
||||
"@babel/helper-skip-transparent-expression-wrappers" "^7.27.1"
|
||||
"@babel/traverse" "^7.28.5"
|
||||
semver "^6.3.1"
|
||||
|
||||
"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.27.1":
|
||||
version "7.27.1"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.1.tgz#05b0882d97ba1d4d03519e4bce615d70afa18c53"
|
||||
@@ -387,6 +432,14 @@
|
||||
"@babel/traverse" "^7.27.1"
|
||||
"@babel/types" "^7.27.1"
|
||||
|
||||
"@babel/helper-member-expression-to-functions@^7.28.5":
|
||||
version "7.28.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz#f3e07a10be37ed7a63461c63e6929575945a6150"
|
||||
integrity sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==
|
||||
dependencies:
|
||||
"@babel/traverse" "^7.28.5"
|
||||
"@babel/types" "^7.28.5"
|
||||
|
||||
"@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.27.1":
|
||||
version "7.27.1"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz#7ef769a323e2655e126673bb6d2d6913bbead204"
|
||||
@@ -452,6 +505,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz#a7054dcc145a967dd4dc8fee845a57c1316c9df8"
|
||||
integrity sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==
|
||||
|
||||
"@babel/helper-validator-identifier@^7.28.5":
|
||||
version "7.28.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4"
|
||||
integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==
|
||||
|
||||
"@babel/helper-validator-option@^7.27.1":
|
||||
version "7.27.1"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz#fa52f5b1e7db1ab049445b421c4471303897702f"
|
||||
@@ -474,6 +532,14 @@
|
||||
"@babel/template" "^7.27.2"
|
||||
"@babel/types" "^7.28.2"
|
||||
|
||||
"@babel/helpers@^7.28.4":
|
||||
version "7.28.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.28.4.tgz#fe07274742e95bdf7cf1443593eeb8926ab63827"
|
||||
integrity sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==
|
||||
dependencies:
|
||||
"@babel/template" "^7.27.2"
|
||||
"@babel/types" "^7.28.4"
|
||||
|
||||
"@babel/parser@^7.27.2", "@babel/parser@^7.28.3":
|
||||
version "7.28.3"
|
||||
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.3.tgz#d2d25b814621bca5fe9d172bc93792547e7a2a71"
|
||||
@@ -481,6 +547,13 @@
|
||||
dependencies:
|
||||
"@babel/types" "^7.28.2"
|
||||
|
||||
"@babel/parser@^7.28.5":
|
||||
version "7.28.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.5.tgz#0b0225ee90362f030efd644e8034c99468893b08"
|
||||
integrity sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==
|
||||
dependencies:
|
||||
"@babel/types" "^7.28.5"
|
||||
|
||||
"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.27.1":
|
||||
version "7.27.1"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz#61dd8a8e61f7eb568268d1b5f129da3eee364bf9"
|
||||
@@ -883,7 +956,7 @@
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.27.1"
|
||||
|
||||
"@babel/plugin-transform-react-display-name@^7.27.1":
|
||||
"@babel/plugin-transform-react-display-name@^7.27.1", "@babel/plugin-transform-react-display-name@^7.28.0":
|
||||
version "7.28.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.28.0.tgz#6f20a7295fea7df42eb42fed8f896813f5b934de"
|
||||
integrity sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA==
|
||||
@@ -997,6 +1070,17 @@
|
||||
"@babel/helper-skip-transparent-expression-wrappers" "^7.27.1"
|
||||
"@babel/plugin-syntax-typescript" "^7.27.1"
|
||||
|
||||
"@babel/plugin-transform-typescript@^7.28.5":
|
||||
version "7.28.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.5.tgz#441c5f9a4a1315039516c6c612fc66d5f4594e72"
|
||||
integrity sha512-x2Qa+v/CuEoX7Dr31iAfr0IhInrVOWZU/2vJMJ00FOR/2nM0BcBEclpaf9sWCDc+v5e9dMrhSH8/atq/kX7+bA==
|
||||
dependencies:
|
||||
"@babel/helper-annotate-as-pure" "^7.27.3"
|
||||
"@babel/helper-create-class-features-plugin" "^7.28.5"
|
||||
"@babel/helper-plugin-utils" "^7.27.1"
|
||||
"@babel/helper-skip-transparent-expression-wrappers" "^7.27.1"
|
||||
"@babel/plugin-syntax-typescript" "^7.27.1"
|
||||
|
||||
"@babel/plugin-transform-unicode-escapes@^7.27.1":
|
||||
version "7.27.1"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz#3e3143f8438aef842de28816ece58780190cf806"
|
||||
@@ -1125,6 +1209,18 @@
|
||||
"@babel/plugin-transform-react-jsx-development" "^7.27.1"
|
||||
"@babel/plugin-transform-react-pure-annotations" "^7.27.1"
|
||||
|
||||
"@babel/preset-react@^7.26.3":
|
||||
version "7.28.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.28.5.tgz#6fcc0400fa79698433d653092c3919bb4b0878d9"
|
||||
integrity sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ==
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.27.1"
|
||||
"@babel/helper-validator-option" "^7.27.1"
|
||||
"@babel/plugin-transform-react-display-name" "^7.28.0"
|
||||
"@babel/plugin-transform-react-jsx" "^7.27.1"
|
||||
"@babel/plugin-transform-react-jsx-development" "^7.27.1"
|
||||
"@babel/plugin-transform-react-pure-annotations" "^7.27.1"
|
||||
|
||||
"@babel/preset-typescript@^7.21.0", "@babel/preset-typescript@^7.25.9":
|
||||
version "7.27.1"
|
||||
resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.27.1.tgz#190742a6428d282306648a55b0529b561484f912"
|
||||
@@ -1136,6 +1232,17 @@
|
||||
"@babel/plugin-transform-modules-commonjs" "^7.27.1"
|
||||
"@babel/plugin-transform-typescript" "^7.27.1"
|
||||
|
||||
"@babel/preset-typescript@^7.26.0":
|
||||
version "7.28.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz#540359efa3028236958466342967522fd8f2a60c"
|
||||
integrity sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.27.1"
|
||||
"@babel/helper-validator-option" "^7.27.1"
|
||||
"@babel/plugin-syntax-jsx" "^7.27.1"
|
||||
"@babel/plugin-transform-modules-commonjs" "^7.27.1"
|
||||
"@babel/plugin-transform-typescript" "^7.28.5"
|
||||
|
||||
"@babel/runtime-corejs3@^7.20.7", "@babel/runtime-corejs3@^7.22.15", "@babel/runtime-corejs3@^7.25.9", "@babel/runtime-corejs3@^7.26.10", "@babel/runtime-corejs3@^7.27.1":
|
||||
version "7.28.3"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.28.3.tgz#8a993bea33c4f03b02b95ca9164dad26aaca125d"
|
||||
@@ -1170,6 +1277,19 @@
|
||||
"@babel/types" "^7.28.2"
|
||||
debug "^4.3.1"
|
||||
|
||||
"@babel/traverse@^7.28.5":
|
||||
version "7.28.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.28.5.tgz#450cab9135d21a7a2ca9d2d35aa05c20e68c360b"
|
||||
integrity sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.27.1"
|
||||
"@babel/generator" "^7.28.5"
|
||||
"@babel/helper-globals" "^7.28.0"
|
||||
"@babel/parser" "^7.28.5"
|
||||
"@babel/template" "^7.27.2"
|
||||
"@babel/types" "^7.28.5"
|
||||
debug "^4.3.1"
|
||||
|
||||
"@babel/types@^7.21.3", "@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.28.2", "@babel/types@^7.4.4":
|
||||
version "7.28.2"
|
||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.2.tgz#da9db0856a9a88e0a13b019881d7513588cf712b"
|
||||
@@ -1178,6 +1298,14 @@
|
||||
"@babel/helper-string-parser" "^7.27.1"
|
||||
"@babel/helper-validator-identifier" "^7.27.1"
|
||||
|
||||
"@babel/types@^7.28.4", "@babel/types@^7.28.5":
|
||||
version "7.28.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.5.tgz#10fc405f60897c35f07e85493c932c7b5ca0592b"
|
||||
integrity sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==
|
||||
dependencies:
|
||||
"@babel/helper-string-parser" "^7.27.1"
|
||||
"@babel/helper-validator-identifier" "^7.28.5"
|
||||
|
||||
"@braintree/sanitize-url@^7.0.4":
|
||||
version "7.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-7.1.1.tgz#15e19737d946559289b915e5dad3b4c28407735e"
|
||||
@@ -2431,6 +2559,16 @@
|
||||
resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-5.5.3.tgz#18e3af6b8eae7984072bbeb0c0858474d7c4cefe"
|
||||
integrity sha512-R11tGE6yIFwqpaIqcfkcg7AICXzFg14+5h5v0TfF/9+RMDL6jhzCy/pxHVOfbALGdtVYdt6JdR21tuxEgl34dw==
|
||||
|
||||
"@fontsource/fira-code@^5.2.7":
|
||||
version "5.2.7"
|
||||
resolved "https://registry.yarnpkg.com/@fontsource/fira-code/-/fira-code-5.2.7.tgz#9ecbd909d53e7196a5d895b601747fe34491fc6a"
|
||||
integrity sha512-tnB9NNund9TwIym8/7DMJe573nlPEQb+fKUV5GL8TBYXjIhDvL0D7mgmNVNQUPhXp+R7RylQeiBdkA4EbOHPGQ==
|
||||
|
||||
"@fontsource/inter@^5.2.8":
|
||||
version "5.2.8"
|
||||
resolved "https://registry.yarnpkg.com/@fontsource/inter/-/inter-5.2.8.tgz#10c95d877d972c7de5bd4592309d42fb6a5e1a5b"
|
||||
integrity sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg==
|
||||
|
||||
"@hapi/hoek@^9.0.0", "@hapi/hoek@^9.3.0":
|
||||
version "9.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb"
|
||||
@@ -2522,6 +2660,14 @@
|
||||
"@jridgewell/sourcemap-codec" "^1.5.0"
|
||||
"@jridgewell/trace-mapping" "^0.3.24"
|
||||
|
||||
"@jridgewell/remapping@^2.3.5":
|
||||
version "2.3.5"
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/remapping/-/remapping-2.3.5.tgz#375c476d1972947851ba1e15ae8f123047445aa1"
|
||||
integrity sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==
|
||||
dependencies:
|
||||
"@jridgewell/gen-mapping" "^0.3.5"
|
||||
"@jridgewell/trace-mapping" "^0.3.24"
|
||||
|
||||
"@jridgewell/resolve-uri@^3.1.0":
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6"
|
||||
@@ -6885,9 +7031,9 @@ docusaurus-plugin-less@^2.0.2:
|
||||
integrity sha512-ez6WSSvGS8HoJslYHeG5SflyShWvHFXeTTHXPBd3H1T3zgq9wp6wD7scXm+rXyyfhFhP5VNiIqhYB78z4OLjwg==
|
||||
|
||||
docusaurus-plugin-openapi-docs@^4.6.0:
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/docusaurus-plugin-openapi-docs/-/docusaurus-plugin-openapi-docs-4.6.0.tgz#2b89a3d23f1836a3691f076860dd67013b4800ab"
|
||||
integrity sha512-wcRUnZca9hRiuAcw2Iz+YUVO4dh01mV2FoAtomRMVlWZIEgw6TA5SqsfHWRd6on/ibvvVS9Lq6GjZTcSjwLcWQ==
|
||||
version "4.7.1"
|
||||
resolved "https://registry.yarnpkg.com/docusaurus-plugin-openapi-docs/-/docusaurus-plugin-openapi-docs-4.7.1.tgz#fb1cf0d30bb49dc7ceb643ea623209bba054cb2a"
|
||||
integrity sha512-RpqvTEnhIfdSuTn/Fa/8bmxeufijLL9HCRb//ELD33AKqEbCw147SKR/CqWu4H4gwi50FZLUbiHKZJbPtXLt9Q==
|
||||
dependencies:
|
||||
"@apidevtools/json-schema-ref-parser" "^11.5.4"
|
||||
"@redocly/openapi-core" "^1.34.3"
|
||||
@@ -6906,9 +7052,9 @@ docusaurus-plugin-openapi-docs@^4.6.0:
|
||||
xml-formatter "^3.6.6"
|
||||
|
||||
docusaurus-theme-openapi-docs@^4.6.0:
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/docusaurus-theme-openapi-docs/-/docusaurus-theme-openapi-docs-4.6.0.tgz#d01965cef49764c861b4c4920c363ac4bf88cb82"
|
||||
integrity sha512-YCgYReVMcrKDTNvM4dh9+i+ies+sGbCwv12TRCPZZbeif7RqTc/5w4rhxEIfp/v0uOAQGL4iXfTSBAMExotbMQ==
|
||||
version "4.7.1"
|
||||
resolved "https://registry.yarnpkg.com/docusaurus-theme-openapi-docs/-/docusaurus-theme-openapi-docs-4.7.1.tgz#bcdb59a76852ed5f9dc77280b38e62de0745d699"
|
||||
integrity sha512-OPydf11LoEY3fdxaoqCVO+qCk7LBo6l6s28UvHJ5mIN/2xu+dOOio9+xnKZ5FIPOlD+dx0gVSKzaVCi/UFTxlg==
|
||||
dependencies:
|
||||
"@hookform/error-message" "^2.0.1"
|
||||
"@reduxjs/toolkit" "^2.8.2"
|
||||
@@ -12475,6 +12621,13 @@ react-redux@^9.2.0:
|
||||
"@types/use-sync-external-store" "^0.0.6"
|
||||
use-sync-external-store "^1.4.0"
|
||||
|
||||
react-resize-detector@7.1.2:
|
||||
version "7.1.2"
|
||||
resolved "https://registry.yarnpkg.com/react-resize-detector/-/react-resize-detector-7.1.2.tgz#8ef975dd8c3d56f9a5160ac382ef7136dcd2d86c"
|
||||
integrity sha512-zXnPJ2m8+6oq9Nn8zsep/orts9vQv3elrpA+R8XTcW7DVVUJ9vwDwMXaBtykAYjMnkCIaOoK9vObyR7ZgFNlOw==
|
||||
dependencies:
|
||||
lodash "^4.17.21"
|
||||
|
||||
react-router-config@^5.1.1:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/react-router-config/-/react-router-config-5.1.1.tgz#0f4263d1a80c6b2dc7b9c1902c9526478194a988"
|
||||
@@ -12530,6 +12683,11 @@ react-syntax-highlighter@^16.0.0:
|
||||
prismjs "^1.30.0"
|
||||
refractor "^5.0.0"
|
||||
|
||||
react-table@^7.8.0:
|
||||
version "7.8.0"
|
||||
resolved "https://registry.yarnpkg.com/react-table/-/react-table-7.8.0.tgz#07858c01c1718c09f7f1aed7034fcfd7bda907d2"
|
||||
integrity sha512-hNaz4ygkZO4bESeFfnfOft73iBUj8K5oKi1EcSHPAibEydfsX2MyU6Z8KCr3mv3C9Kqqh71U+DhZkFvibbnPbA==
|
||||
|
||||
"react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0":
|
||||
version "19.1.1"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-19.1.1.tgz#06d9149ec5e083a67f9a1e39ce97b06a03b644af"
|
||||
|
||||
@@ -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
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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 },
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
</>
|
||||
|
||||
@@ -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} />,
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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 = [
|
||||
{
|
||||
|
||||
@@ -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'> & {
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}`,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
);
|
||||
}`,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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!')}
|
||||
/>
|
||||
);
|
||||
}`,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}`,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}`,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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' },
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>}
|
||||
/>
|
||||
);
|
||||
}`,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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' },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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} />;
|
||||
}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
);
|
||||
}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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' },
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 = () => (
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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.' },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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 />;
|
||||
}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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'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>
|
||||
);
|
||||
}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -17,55 +17,12 @@ specific language governing permissions and limitations
|
||||
under the License.
|
||||
-->
|
||||
|
||||
## @superset-ui/demo
|
||||
# @superset-ui/demo
|
||||
|
||||
[](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.
|
||||
|
||||
Reference in New Issue
Block a user