From b198c9474adcddd1d0296fcd8e7ef91f6a8684eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9D=9E=E6=B3=95=E6=93=8D=E4=BD=9C?= Date: Thu, 23 Oct 2025 11:00:45 +0800 Subject: [PATCH] chore: improve storybooks (#27306) --- web/.storybook/main.ts | 9 +- .../utils/audio-player-manager.mock.ts | 64 ++++++ .../base/action-button/index.stories.tsx | 2 +- .../base/audio-btn/index.stories.tsx | 75 ++++++ .../auto-height-textarea/index.stories.tsx | 2 +- .../base/block-input/index.stories.tsx | 2 +- .../base/button/add-button.stories.tsx | 52 +++++ .../components/base/button/index.stories.tsx | 2 +- .../base/button/sync-button.stories.tsx | 57 +++++ .../base/chat/chat/answer/index.stories.tsx | 2 +- .../base/chat/chat/question.stories.tsx | 2 +- .../base/checkbox/index.stories.tsx | 2 +- .../components/base/confirm/index.stories.tsx | 2 +- .../base/content-dialog/index.stories.tsx | 110 +++++++++ .../components/base/dialog/index.stories.tsx | 151 ++++++++++++ .../base/input-number/index.stories.tsx | 2 +- .../components/base/input/index.stories.tsx | 2 +- .../base/modal-like-wrap/index.stories.tsx | 125 ++++++++++ .../components/base/modal/index.stories.tsx | 133 +++++++++++ .../components/base/modal/modal.stories.tsx | 216 ++++++++++++++++++ .../base/new-audio-button/index.stories.tsx | 67 ++++++ .../base/prompt-editor/index.stories.tsx | 2 +- .../base/radio-card/index.stories.tsx | 2 +- .../components/base/radio/index.stories.tsx | 2 +- .../base/search-input/index.stories.tsx | 2 +- .../components/base/select/index.stories.tsx | 2 +- .../components/base/slider/index.stories.tsx | 2 +- .../components/base/switch/index.stories.tsx | 2 +- .../base/tag-input/index.stories.tsx | 2 +- .../base/textarea/index.stories.tsx | 2 +- .../base/voice-input/index.stories.tsx | 2 +- .../with-input-validation/index.stories.tsx | 2 +- 32 files changed, 1077 insertions(+), 24 deletions(-) create mode 100644 web/.storybook/utils/audio-player-manager.mock.ts create mode 100644 web/app/components/base/audio-btn/index.stories.tsx create mode 100644 web/app/components/base/button/add-button.stories.tsx create mode 100644 web/app/components/base/button/sync-button.stories.tsx create mode 100644 web/app/components/base/content-dialog/index.stories.tsx create mode 100644 web/app/components/base/dialog/index.stories.tsx create mode 100644 web/app/components/base/modal-like-wrap/index.stories.tsx create mode 100644 web/app/components/base/modal/index.stories.tsx create mode 100644 web/app/components/base/modal/modal.stories.tsx create mode 100644 web/app/components/base/new-audio-button/index.stories.tsx diff --git a/web/.storybook/main.ts b/web/.storybook/main.ts index 57abae42ab..ca56261431 100644 --- a/web/.storybook/main.ts +++ b/web/.storybook/main.ts @@ -1,5 +1,8 @@ import type { StorybookConfig } from '@storybook/nextjs' import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const storybookDir = path.dirname(fileURLToPath(import.meta.url)) const config: StorybookConfig = { stories: ['../app/components/**/*.stories.@(js|jsx|mjs|ts|tsx)'], @@ -32,9 +35,9 @@ const config: StorybookConfig = { config.resolve.alias = { ...config.resolve.alias, // Mock the plugin index files to avoid circular dependencies - [path.resolve(__dirname, '../app/components/base/prompt-editor/plugins/context-block/index.tsx')]: path.resolve(__dirname, '__mocks__/context-block.tsx'), - [path.resolve(__dirname, '../app/components/base/prompt-editor/plugins/history-block/index.tsx')]: path.resolve(__dirname, '__mocks__/history-block.tsx'), - [path.resolve(__dirname, '../app/components/base/prompt-editor/plugins/query-block/index.tsx')]: path.resolve(__dirname, '__mocks__/query-block.tsx'), + [path.resolve(storybookDir, '../app/components/base/prompt-editor/plugins/context-block/index.tsx')]: path.resolve(storybookDir, '__mocks__/context-block.tsx'), + [path.resolve(storybookDir, '../app/components/base/prompt-editor/plugins/history-block/index.tsx')]: path.resolve(storybookDir, '__mocks__/history-block.tsx'), + [path.resolve(storybookDir, '../app/components/base/prompt-editor/plugins/query-block/index.tsx')]: path.resolve(storybookDir, '__mocks__/query-block.tsx'), } return config }, diff --git a/web/.storybook/utils/audio-player-manager.mock.ts b/web/.storybook/utils/audio-player-manager.mock.ts new file mode 100644 index 0000000000..aca8b56b76 --- /dev/null +++ b/web/.storybook/utils/audio-player-manager.mock.ts @@ -0,0 +1,64 @@ +import { AudioPlayerManager } from '@/app/components/base/audio-btn/audio.player.manager' + +type PlayerCallback = ((event: string) => void) | null + +class MockAudioPlayer { + private callback: PlayerCallback = null + private finishTimer?: ReturnType + + public setCallback(callback: PlayerCallback) { + this.callback = callback + } + + public playAudio() { + this.clearTimer() + this.callback?.('play') + this.finishTimer = setTimeout(() => { + this.callback?.('ended') + }, 2000) + } + + public pauseAudio() { + this.clearTimer() + this.callback?.('paused') + } + + private clearTimer() { + if (this.finishTimer) + clearTimeout(this.finishTimer) + } +} + +class MockAudioPlayerManager { + private readonly player = new MockAudioPlayer() + + public getAudioPlayer( + _url: string, + _isPublic: boolean, + _id: string | undefined, + _msgContent: string | null | undefined, + _voice: string | undefined, + callback: PlayerCallback, + ) { + this.player.setCallback(callback) + return this.player + } + + public resetMsgId() { + // No-op for the mock + } +} + +export const ensureMockAudioManager = () => { + const managerAny = AudioPlayerManager as unknown as { + getInstance: () => AudioPlayerManager + __isStorybookMockInstalled?: boolean + } + + if (managerAny.__isStorybookMockInstalled) + return + + const mock = new MockAudioPlayerManager() + managerAny.getInstance = () => mock as unknown as AudioPlayerManager + managerAny.__isStorybookMockInstalled = true +} diff --git a/web/app/components/base/action-button/index.stories.tsx b/web/app/components/base/action-button/index.stories.tsx index c174adbc73..dd826c41ba 100644 --- a/web/app/components/base/action-button/index.stories.tsx +++ b/web/app/components/base/action-button/index.stories.tsx @@ -3,7 +3,7 @@ import { RiAddLine, RiDeleteBinLine, RiEditLine, RiMore2Fill, RiSaveLine, RiShar import ActionButton, { ActionButtonState } from '.' const meta = { - title: 'Base/ActionButton', + title: 'Base/Button/ActionButton', component: ActionButton, parameters: { layout: 'centered', diff --git a/web/app/components/base/audio-btn/index.stories.tsx b/web/app/components/base/audio-btn/index.stories.tsx new file mode 100644 index 0000000000..8dc82d3413 --- /dev/null +++ b/web/app/components/base/audio-btn/index.stories.tsx @@ -0,0 +1,75 @@ +import type { Meta, StoryObj } from '@storybook/nextjs' +import { useEffect } from 'react' +import type { ComponentProps } from 'react' +import AudioBtn from '.' +import { ensureMockAudioManager } from '../../../../.storybook/utils/audio-player-manager.mock' + +ensureMockAudioManager() + +const StoryWrapper = (props: ComponentProps) => { + useEffect(() => { + ensureMockAudioManager() + }, []) + + return ( +
+ + Click to toggle playback +
+ ) +} + +const meta = { + title: 'Base/Button/AudioBtn', + component: AudioBtn, + tags: ['autodocs'], + parameters: { + layout: 'centered', + docs: { + description: { + component: 'Audio playback toggle that streams assistant responses. The story uses a mocked audio player so you can inspect loading and playback states without calling the real API.', + }, + }, + nextjs: { + appDirectory: true, + navigation: { + pathname: '/apps/demo-app/text-to-audio', + params: { appId: 'demo-app' }, + }, + }, + }, + argTypes: { + id: { + control: 'text', + description: 'Message identifier used to scope the audio stream.', + }, + value: { + control: 'text', + description: 'Text content that would be converted to speech.', + }, + voice: { + control: 'text', + description: 'Voice profile used for playback.', + }, + isAudition: { + control: 'boolean', + description: 'Switches to the audition style with minimal padding.', + }, + className: { + control: 'text', + description: 'Optional custom class for the wrapper.', + }, + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = { + render: args => , + args: { + id: 'message-1', + value: 'This is an audio preview for the current assistant response.', + voice: 'alloy', + }, +} diff --git a/web/app/components/base/auto-height-textarea/index.stories.tsx b/web/app/components/base/auto-height-textarea/index.stories.tsx index dcbcb253c6..a9234fac9d 100644 --- a/web/app/components/base/auto-height-textarea/index.stories.tsx +++ b/web/app/components/base/auto-height-textarea/index.stories.tsx @@ -3,7 +3,7 @@ import { useState } from 'react' import AutoHeightTextarea from '.' const meta = { - title: 'Base/AutoHeightTextarea', + title: 'Base/Input/AutoHeightTextarea', component: AutoHeightTextarea, parameters: { layout: 'centered', diff --git a/web/app/components/base/block-input/index.stories.tsx b/web/app/components/base/block-input/index.stories.tsx index 0685f4150f..5f1967b9d0 100644 --- a/web/app/components/base/block-input/index.stories.tsx +++ b/web/app/components/base/block-input/index.stories.tsx @@ -3,7 +3,7 @@ import { useState } from 'react' import BlockInput from '.' const meta = { - title: 'Base/BlockInput', + title: 'Base/Input/BlockInput', component: BlockInput, parameters: { layout: 'centered', diff --git a/web/app/components/base/button/add-button.stories.tsx b/web/app/components/base/button/add-button.stories.tsx new file mode 100644 index 0000000000..a46441aefe --- /dev/null +++ b/web/app/components/base/button/add-button.stories.tsx @@ -0,0 +1,52 @@ +import type { Meta, StoryObj } from '@storybook/nextjs' +import AddButton from './add-button' + +const meta = { + title: 'Base/Button/AddButton', + component: AddButton, + parameters: { + layout: 'centered', + docs: { + description: { + component: 'Compact icon-only button used for inline “add” actions in lists, cards, and modals.', + }, + }, + }, + tags: ['autodocs'], + argTypes: { + className: { + control: 'text', + description: 'Extra classes appended to the clickable container.', + }, + onClick: { + control: false, + description: 'Triggered when the add button is pressed.', + }, + }, + args: { + onClick: () => console.log('Add button clicked'), + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = { + args: { + className: 'bg-white/80 shadow-sm backdrop-blur-sm', + }, +} + +export const InToolbar: Story = { + render: args => ( +
+ Attachments +
+ +
+
+ ), + args: { + className: 'border border-dashed border-primary-200', + }, +} diff --git a/web/app/components/base/button/index.stories.tsx b/web/app/components/base/button/index.stories.tsx index e51b928e5e..f369e2f71a 100644 --- a/web/app/components/base/button/index.stories.tsx +++ b/web/app/components/base/button/index.stories.tsx @@ -4,7 +4,7 @@ import { RocketLaunchIcon } from '@heroicons/react/20/solid' import { Button } from '.' const meta = { - title: 'Base/Button', + title: 'Base/Button/Button', component: Button, parameters: { layout: 'centered', diff --git a/web/app/components/base/button/sync-button.stories.tsx b/web/app/components/base/button/sync-button.stories.tsx new file mode 100644 index 0000000000..d55a7acf47 --- /dev/null +++ b/web/app/components/base/button/sync-button.stories.tsx @@ -0,0 +1,57 @@ +import type { Meta, StoryObj } from '@storybook/nextjs' +import SyncButton from './sync-button' + +const meta = { + title: 'Base/Button/SyncButton', + component: SyncButton, + parameters: { + layout: 'centered', + docs: { + description: { + component: 'Icon-only refresh button that surfaces a tooltip and is used for manual sync actions across the UI.', + }, + }, + }, + tags: ['autodocs'], + argTypes: { + className: { + control: 'text', + description: 'Additional classes appended to the clickable container.', + }, + popupContent: { + control: 'text', + description: 'Tooltip text shown on hover.', + }, + onClick: { + control: false, + description: 'Triggered when the sync button is pressed.', + }, + }, + args: { + popupContent: 'Sync now', + onClick: () => console.log('Sync button clicked'), + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = { + args: { + className: 'bg-white/80 shadow-sm backdrop-blur-sm', + }, +} + +export const InHeader: Story = { + render: args => ( +
+ Logs +
+ +
+
+ ), + args: { + popupContent: 'Refresh logs', + }, +} diff --git a/web/app/components/base/chat/chat/answer/index.stories.tsx b/web/app/components/base/chat/chat/answer/index.stories.tsx index 02d0f015b5..822bdf7326 100644 --- a/web/app/components/base/chat/chat/answer/index.stories.tsx +++ b/web/app/components/base/chat/chat/answer/index.stories.tsx @@ -6,7 +6,7 @@ import { markdownContentSVG } from './__mocks__/markdownContentSVG' import Answer from '.' const meta = { - title: 'Base/Chat Answer', + title: 'Base/Chat/Chat Answer', component: Answer, parameters: { layout: 'fullscreen', diff --git a/web/app/components/base/chat/chat/question.stories.tsx b/web/app/components/base/chat/chat/question.stories.tsx index 6474add9df..0b84ee91a8 100644 --- a/web/app/components/base/chat/chat/question.stories.tsx +++ b/web/app/components/base/chat/chat/question.stories.tsx @@ -5,7 +5,7 @@ import Question from './question' import { User } from '@/app/components/base/icons/src/public/avatar' const meta = { - title: 'Base/Chat Question', + title: 'Base/Chat/Chat Question', component: Question, parameters: { layout: 'centered', diff --git a/web/app/components/base/checkbox/index.stories.tsx b/web/app/components/base/checkbox/index.stories.tsx index 65fa8e1b97..ba928baa6f 100644 --- a/web/app/components/base/checkbox/index.stories.tsx +++ b/web/app/components/base/checkbox/index.stories.tsx @@ -13,7 +13,7 @@ const createToggleItem = ( } const meta = { - title: 'Base/Checkbox', + title: 'Base/Input/Checkbox', component: Checkbox, parameters: { layout: 'centered', diff --git a/web/app/components/base/confirm/index.stories.tsx b/web/app/components/base/confirm/index.stories.tsx index a524137b79..9ec21cbd50 100644 --- a/web/app/components/base/confirm/index.stories.tsx +++ b/web/app/components/base/confirm/index.stories.tsx @@ -4,7 +4,7 @@ import Confirm from '.' import Button from '../button' const meta = { - title: 'Base/Confirm', + title: 'Base/Dialog/Confirm', component: Confirm, parameters: { layout: 'centered', diff --git a/web/app/components/base/content-dialog/index.stories.tsx b/web/app/components/base/content-dialog/index.stories.tsx new file mode 100644 index 0000000000..29b3914704 --- /dev/null +++ b/web/app/components/base/content-dialog/index.stories.tsx @@ -0,0 +1,110 @@ +import type { Meta, StoryObj } from '@storybook/nextjs' +import { useEffect, useState } from 'react' +import ContentDialog from '.' + +type Props = React.ComponentProps + +const meta = { + title: 'Base/Dialog/ContentDialog', + component: ContentDialog, + parameters: { + layout: 'fullscreen', + docs: { + description: { + component: 'Sliding panel overlay used in the app detail view. Includes dimmed backdrop and animated entrance/exit transitions.', + }, + }, + }, + tags: ['autodocs'], + argTypes: { + className: { + control: 'text', + description: 'Additional classes applied to the sliding panel container.', + }, + show: { + control: 'boolean', + description: 'Controls visibility of the dialog.', + }, + onClose: { + control: false, + description: 'Invoked when the overlay/backdrop is clicked.', + }, + }, + args: { + show: false, + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +const DemoWrapper = (props: Props) => { + const [open, setOpen] = useState(props.show) + + useEffect(() => { + setOpen(props.show) + }, [props.show]) + + return ( +
+
+ +
+ + { + props.onClose?.() + setOpen(false) + }} + > +
+

Plan summary

+

+ Use this area to present rich content for the selected run, configuration details, or + any supporting context. +

+
+ Scrollable placeholder content. Add domain-specific information, activity logs, or + editors in the real application. +
+
+ + +
+
+
+
+ ) +} + +export const Default: Story = { + render: args => , +} + +export const NarrowPanel: Story = { + render: args => , + args: { + className: 'max-w-[420px]', + }, + parameters: { + docs: { + description: { + story: 'Applies a custom width class to show the dialog as a narrower information panel.', + }, + }, + }, +} diff --git a/web/app/components/base/dialog/index.stories.tsx b/web/app/components/base/dialog/index.stories.tsx new file mode 100644 index 0000000000..62ae7c00ce --- /dev/null +++ b/web/app/components/base/dialog/index.stories.tsx @@ -0,0 +1,151 @@ +import type { Meta, StoryObj } from '@storybook/nextjs' +import { useEffect, useState } from 'react' +import Dialog from '.' + +const meta = { + title: 'Base/Dialog/Dialog', + component: Dialog, + parameters: { + layout: 'fullscreen', + docs: { + description: { + component: 'Modal dialog built on Headless UI. Provides animated overlay, title slot, and optional footer region.', + }, + }, + }, + tags: ['autodocs'], + argTypes: { + className: { + control: 'text', + description: 'Additional classes applied to the panel.', + }, + titleClassName: { + control: 'text', + description: 'Extra classes for the title element.', + }, + bodyClassName: { + control: 'text', + description: 'Extra classes for the content area.', + }, + footerClassName: { + control: 'text', + description: 'Extra classes for the footer container.', + }, + title: { + control: 'text', + description: 'Dialog title.', + }, + show: { + control: 'boolean', + description: 'Controls visibility of the dialog.', + }, + onClose: { + control: false, + description: 'Called when the dialog backdrop or close handler fires.', + }, + }, + args: { + title: 'Manage API Keys', + show: false, + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +const DialogDemo = (props: React.ComponentProps) => { + const [open, setOpen] = useState(props.show) + useEffect(() => { + setOpen(props.show) + }, [props.show]) + + return ( +
+ + + { + props.onClose?.() + setOpen(false) + }} + > +
+

+ Centralize API key management for collaborators. You can revoke, rotate, or generate new keys directly from this dialog. +

+
+ This placeholder area represents a form or table that would live inside the dialog body. +
+
+
+
+ ) +} + +export const Default: Story = { + render: args => , + args: { + footer: ( + <> + + + + ), + }, +} + +export const WithoutFooter: Story = { + render: args => , + args: { + footer: undefined, + title: 'Read-only summary', + }, + parameters: { + docs: { + description: { + story: 'Demonstrates the dialog when no footer actions are provided.', + }, + }, + }, +} + +export const CustomStyling: Story = { + render: args => , + args: { + className: 'max-w-[560px] bg-white/95 backdrop-blur', + bodyClassName: 'bg-gray-50 rounded-xl p-5', + footerClassName: 'justify-between px-4 pb-4 pt-4', + titleClassName: 'text-lg text-primary-600', + footer: ( + <> + Last synced 2 minutes ago +
+ + +
+ + ), + }, + parameters: { + docs: { + description: { + story: 'Applies custom classes to the panel, body, title, and footer to match different surfaces.', + }, + }, + }, +} diff --git a/web/app/components/base/input-number/index.stories.tsx b/web/app/components/base/input-number/index.stories.tsx index 9bb3ec1f8c..aa075b0ff1 100644 --- a/web/app/components/base/input-number/index.stories.tsx +++ b/web/app/components/base/input-number/index.stories.tsx @@ -3,7 +3,7 @@ import { useState } from 'react' import { InputNumber } from '.' const meta = { - title: 'Base/InputNumber', + title: 'Base/Input/InputNumber', component: InputNumber, parameters: { layout: 'centered', diff --git a/web/app/components/base/input/index.stories.tsx b/web/app/components/base/input/index.stories.tsx index cd857bc180..04df0bf943 100644 --- a/web/app/components/base/input/index.stories.tsx +++ b/web/app/components/base/input/index.stories.tsx @@ -3,7 +3,7 @@ import { useState } from 'react' import Input from '.' const meta = { - title: 'Base/Input', + title: 'Base/Input/Input', component: Input, parameters: { layout: 'centered', diff --git a/web/app/components/base/modal-like-wrap/index.stories.tsx b/web/app/components/base/modal-like-wrap/index.stories.tsx new file mode 100644 index 0000000000..bf027e0db4 --- /dev/null +++ b/web/app/components/base/modal-like-wrap/index.stories.tsx @@ -0,0 +1,125 @@ +import type { Meta, StoryObj } from '@storybook/nextjs' +import ModalLikeWrap from '.' + +const meta = { + title: 'Base/Dialog/ModalLikeWrap', + component: ModalLikeWrap, + parameters: { + layout: 'centered', + docs: { + description: { + component: 'Compact “modal-like” card used in wizards. Provides header actions, optional back slot, and confirm/cancel buttons.', + }, + }, + }, + tags: ['autodocs'], + argTypes: { + title: { + control: 'text', + description: 'Header title text.', + }, + className: { + control: 'text', + description: 'Additional classes on the wrapper.', + }, + beforeHeader: { + control: false, + description: 'Slot rendered before the header (commonly a back link).', + }, + hideCloseBtn: { + control: 'boolean', + description: 'Hides the top-right close icon when true.', + }, + children: { + control: false, + }, + onClose: { + control: false, + }, + onConfirm: { + control: false, + }, + }, + args: { + title: 'Create dataset field', + hideCloseBtn: false, + onClose: () => console.log('close'), + onConfirm: () => console.log('confirm'), + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +const BaseContent = () => ( +
+

+ Describe the new field your dataset should collect. Provide a clear label and optional helper text. +

+
+ Form inputs would be placed here in the real flow. +
+
+) + +export const Default: Story = { + render: args => ( + + + + ), +} + +export const WithBackLink: Story = { + render: args => ( + console.log('back')} + > + {'<'} + Back + + )} + > + + + ), + args: { + title: 'Select metadata type', + }, + parameters: { + docs: { + description: { + story: 'Demonstrates feeding content into `beforeHeader` while hiding the close button.', + }, + }, + }, +} + +export const CustomWidth: Story = { + render: args => ( + + +
+ Tip: metadata keys may only include letters, numbers, and underscores. +
+
+ ), + args: { + title: 'Advanced configuration', + }, + parameters: { + docs: { + description: { + story: 'Applies extra width and helper messaging to emulate configuration panels.', + }, + }, + }, +} diff --git a/web/app/components/base/modal/index.stories.tsx b/web/app/components/base/modal/index.stories.tsx new file mode 100644 index 0000000000..e561acebbb --- /dev/null +++ b/web/app/components/base/modal/index.stories.tsx @@ -0,0 +1,133 @@ +import type { Meta, StoryObj } from '@storybook/nextjs' +import { useEffect, useState } from 'react' +import Modal from '.' + +const meta = { + title: 'Base/Dialog/Modal', + component: Modal, + parameters: { + layout: 'fullscreen', + docs: { + description: { + component: 'Lightweight modal wrapper with optional header/description, close icon, and high-priority stacking for dropdown overlays.', + }, + }, + }, + tags: ['autodocs'], + argTypes: { + className: { + control: 'text', + description: 'Extra classes applied to the modal panel.', + }, + wrapperClassName: { + control: 'text', + description: 'Additional wrapper classes for the dialog.', + }, + isShow: { + control: 'boolean', + description: 'Controls whether the modal is visible.', + }, + title: { + control: 'text', + description: 'Heading displayed at the top of the modal.', + }, + description: { + control: 'text', + description: 'Secondary text beneath the title.', + }, + closable: { + control: 'boolean', + description: 'Whether the close icon should be shown.', + }, + overflowVisible: { + control: 'boolean', + description: 'Allows content to overflow the modal panel.', + }, + highPriority: { + control: 'boolean', + description: 'Lifts the modal above other high z-index elements like dropdowns.', + }, + onClose: { + control: false, + description: 'Callback invoked when the modal requests to close.', + }, + }, + args: { + isShow: false, + title: 'Create new API key', + description: 'Generate a scoped key for this workspace. You can revoke it at any time.', + closable: true, + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +const ModalDemo = (props: React.ComponentProps) => { + const [open, setOpen] = useState(props.isShow) + + useEffect(() => { + setOpen(props.isShow) + }, [props.isShow]) + + return ( +
+ + + { + props.onClose?.() + setOpen(false) + }} + > +
+

+ Provide a descriptive name for this key so collaborators know its purpose. Restrict usage with scopes to limit access. +

+
+ Form fields and validation messaging would appear here. This placeholder keeps the story lightweight. +
+
+
+ + +
+
+
+ ) +} + +export const Default: Story = { + render: args => , +} + +export const HighPriorityOverflow: Story = { + render: args => , + args: { + highPriority: true, + overflowVisible: true, + description: 'Demonstrates the modal configured to sit above dropdowns while letting the body content overflow.', + className: 'max-w-[540px]', + }, + parameters: { + docs: { + description: { + story: 'Shows the modal with `highPriority` and `overflowVisible` enabled, useful when nested within complex surfaces.', + }, + }, + }, +} diff --git a/web/app/components/base/modal/modal.stories.tsx b/web/app/components/base/modal/modal.stories.tsx new file mode 100644 index 0000000000..3e5be78a5b --- /dev/null +++ b/web/app/components/base/modal/modal.stories.tsx @@ -0,0 +1,216 @@ +import type { Meta, StoryObj } from '@storybook/nextjs' +import { useEffect, useState } from 'react' +import Modal from './modal' + +const meta = { + title: 'Base/Dialog/RichModal', + component: Modal, + parameters: { + layout: 'fullscreen', + docs: { + description: { + component: 'Full-featured modal with header, subtitle, customizable footer buttons, and optional extra action.', + }, + }, + }, + tags: ['autodocs'], + argTypes: { + size: { + control: 'radio', + options: ['sm', 'md'], + description: 'Defines the panel width.', + }, + title: { + control: 'text', + description: 'Primary heading text.', + }, + subTitle: { + control: 'text', + description: 'Secondary text below the title.', + }, + confirmButtonText: { + control: 'text', + description: 'Label for the confirm button.', + }, + cancelButtonText: { + control: 'text', + description: 'Label for the cancel button.', + }, + showExtraButton: { + control: 'boolean', + description: 'Whether to render the extra button.', + }, + extraButtonText: { + control: 'text', + description: 'Label for the extra button.', + }, + extraButtonVariant: { + control: 'select', + options: ['primary', 'warning', 'secondary', 'secondary-accent', 'ghost', 'ghost-accent', 'tertiary'], + description: 'Visual style for the extra button.', + }, + disabled: { + control: 'boolean', + description: 'Disables footer actions when true.', + }, + footerSlot: { + control: false, + }, + bottomSlot: { + control: false, + }, + onClose: { + control: false, + description: 'Handler fired when the close icon or backdrop is clicked.', + }, + onConfirm: { + control: false, + description: 'Handler fired when confirm is pressed.', + }, + onCancel: { + control: false, + description: 'Handler fired when cancel is pressed.', + }, + onExtraButtonClick: { + control: false, + description: 'Handler fired when the extra button is pressed.', + }, + children: { + control: false, + }, + }, + args: { + size: 'sm', + title: 'Delete integration', + subTitle: 'Disabling this integration will revoke access tokens and webhooks.', + confirmButtonText: 'Delete integration', + cancelButtonText: 'Cancel', + showExtraButton: false, + extraButtonText: 'Disable temporarily', + extraButtonVariant: 'warning', + disabled: false, + onClose: () => console.log('Modal closed'), + onConfirm: () => console.log('Confirm pressed'), + onCancel: () => console.log('Cancel pressed'), + onExtraButtonClick: () => console.log('Extra button pressed'), + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +type ModalProps = React.ComponentProps + +const ModalDemo = (props: ModalProps) => { + const [open, setOpen] = useState(false) + + useEffect(() => { + if (props.disabled && open) + setOpen(false) + }, [props.disabled, open]) + + const { + onClose, + onConfirm, + onCancel, + onExtraButtonClick, + children, + ...rest + } = props + + const handleClose = () => { + onClose?.() + setOpen(false) + } + + const handleConfirm = () => { + onConfirm?.() + setOpen(false) + } + + const handleCancel = () => { + onCancel?.() + setOpen(false) + } + + const handleExtra = () => { + onExtraButtonClick?.() + } + + return ( +
+ + + {open && ( + +

+ Removing integrations immediately stops workflow automations related to this connection. + Make sure no scheduled jobs depend on this integration before proceeding. +

+
    +
  • All API credentials issued by this integration will be revoked.
  • +
  • Historical logs remain accessible for auditing.
  • +
  • You can re-enable the integration later with fresh credentials.
  • +
+
+ )} + /> + )} + + ) +} + +export const Default: Story = { + render: args => , +} + +export const WithExtraAction: Story = { + render: args => , + args: { + showExtraButton: true, + extraButtonVariant: 'secondary', + extraButtonText: 'Disable only', + footerSlot: ( + Last synced 5 minutes ago + ), + }, + parameters: { + docs: { + description: { + story: 'Illustrates the optional extra button and footer slot for advanced workflows.', + }, + }, + }, +} + +export const MediumSized: Story = { + render: args => , + args: { + size: 'md', + subTitle: 'Use the larger width to surface forms with more fields or supporting descriptions.', + bottomSlot: ( +
+ Need finer control? Configure automation rules in the integration settings page. +
+ ), + }, + parameters: { + docs: { + description: { + story: 'Shows the medium sized panel and a populated `bottomSlot` for supplemental messaging.', + }, + }, + }, +} diff --git a/web/app/components/base/new-audio-button/index.stories.tsx b/web/app/components/base/new-audio-button/index.stories.tsx new file mode 100644 index 0000000000..d2f9b8b4d5 --- /dev/null +++ b/web/app/components/base/new-audio-button/index.stories.tsx @@ -0,0 +1,67 @@ +import type { Meta, StoryObj } from '@storybook/nextjs' +import { useEffect } from 'react' +import type { ComponentProps } from 'react' +import AudioBtn from '.' +import { ensureMockAudioManager } from '../../../../.storybook/utils/audio-player-manager.mock' + +ensureMockAudioManager() + +const StoryWrapper = (props: ComponentProps) => { + useEffect(() => { + ensureMockAudioManager() + }, []) + + return ( +
+ + Audio toggle using ActionButton styling +
+ ) +} + +const meta = { + title: 'Base/Button/NewAudioButton', + component: AudioBtn, + tags: ['autodocs'], + parameters: { + layout: 'centered', + docs: { + description: { + component: 'Updated audio playback trigger styled with `ActionButton`. Behaves like the legacy audio button but adopts the new button design system.', + }, + }, + nextjs: { + appDirectory: true, + navigation: { + pathname: '/apps/demo-app/text-to-audio', + params: { appId: 'demo-app' }, + }, + }, + }, + argTypes: { + id: { + control: 'text', + description: 'Message identifier used by the audio request.', + }, + value: { + control: 'text', + description: 'Prompt or response text that will be converted to speech.', + }, + voice: { + control: 'text', + description: 'Voice profile for the generated speech.', + }, + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = { + render: args => , + args: { + id: 'message-1', + value: 'Listen to the latest assistant message.', + voice: 'alloy', + }, +} diff --git a/web/app/components/base/prompt-editor/index.stories.tsx b/web/app/components/base/prompt-editor/index.stories.tsx index 17b04e4af0..e0d0777306 100644 --- a/web/app/components/base/prompt-editor/index.stories.tsx +++ b/web/app/components/base/prompt-editor/index.stories.tsx @@ -25,7 +25,7 @@ const PromptEditorMock = ({ value, onChange, placeholder, editable, compact, cla } const meta = { - title: 'Base/PromptEditor', + title: 'Base/Input/PromptEditor', component: PromptEditorMock, parameters: { layout: 'centered', diff --git a/web/app/components/base/radio-card/index.stories.tsx b/web/app/components/base/radio-card/index.stories.tsx index e129cd7033..bb45db622c 100644 --- a/web/app/components/base/radio-card/index.stories.tsx +++ b/web/app/components/base/radio-card/index.stories.tsx @@ -4,7 +4,7 @@ import { RiCloudLine, RiCpuLine, RiDatabase2Line, RiLightbulbLine, RiRocketLine, import RadioCard from '.' const meta = { - title: 'Base/RadioCard', + title: 'Base/Input/RadioCard', component: RadioCard, parameters: { layout: 'centered', diff --git a/web/app/components/base/radio/index.stories.tsx b/web/app/components/base/radio/index.stories.tsx index 01f7263d74..0f917320bb 100644 --- a/web/app/components/base/radio/index.stories.tsx +++ b/web/app/components/base/radio/index.stories.tsx @@ -3,7 +3,7 @@ import { useState } from 'react' import Radio from '.' const meta = { - title: 'Base/Radio', + title: 'Base/Input/Radio', component: Radio, parameters: { layout: 'centered', diff --git a/web/app/components/base/search-input/index.stories.tsx b/web/app/components/base/search-input/index.stories.tsx index 99d60d52ff..eb051f892f 100644 --- a/web/app/components/base/search-input/index.stories.tsx +++ b/web/app/components/base/search-input/index.stories.tsx @@ -3,7 +3,7 @@ import { useState } from 'react' import SearchInput from '.' const meta = { - title: 'Base/SearchInput', + title: 'Base/Input/SearchInput', component: SearchInput, parameters: { layout: 'centered', diff --git a/web/app/components/base/select/index.stories.tsx b/web/app/components/base/select/index.stories.tsx index 2a107155a5..48a715498b 100644 --- a/web/app/components/base/select/index.stories.tsx +++ b/web/app/components/base/select/index.stories.tsx @@ -4,7 +4,7 @@ import Select, { PortalSelect, SimpleSelect } from '.' import type { Item } from '.' const meta = { - title: 'Base/Select', + title: 'Base/Input/Select', component: SimpleSelect, parameters: { layout: 'centered', diff --git a/web/app/components/base/slider/index.stories.tsx b/web/app/components/base/slider/index.stories.tsx index d350877d18..691c75d7ad 100644 --- a/web/app/components/base/slider/index.stories.tsx +++ b/web/app/components/base/slider/index.stories.tsx @@ -3,7 +3,7 @@ import { useState } from 'react' import Slider from '.' const meta = { - title: 'Base/Slider', + title: 'Base/Input/Slider', component: Slider, parameters: { layout: 'centered', diff --git a/web/app/components/base/switch/index.stories.tsx b/web/app/components/base/switch/index.stories.tsx index aaeab4c41f..2753a6a309 100644 --- a/web/app/components/base/switch/index.stories.tsx +++ b/web/app/components/base/switch/index.stories.tsx @@ -3,7 +3,7 @@ import { useState } from 'react' import Switch from '.' const meta = { - title: 'Base/Switch', + title: 'Base/Input/Switch', component: Switch, parameters: { layout: 'centered', diff --git a/web/app/components/base/tag-input/index.stories.tsx b/web/app/components/base/tag-input/index.stories.tsx index dacb222c8c..bbb314cf3a 100644 --- a/web/app/components/base/tag-input/index.stories.tsx +++ b/web/app/components/base/tag-input/index.stories.tsx @@ -3,7 +3,7 @@ import { useState } from 'react' import TagInput from '.' const meta = { - title: 'Base/TagInput', + title: 'Base/Input/TagInput', component: TagInput, parameters: { layout: 'centered', diff --git a/web/app/components/base/textarea/index.stories.tsx b/web/app/components/base/textarea/index.stories.tsx index d03b3decb7..ec27aac22b 100644 --- a/web/app/components/base/textarea/index.stories.tsx +++ b/web/app/components/base/textarea/index.stories.tsx @@ -3,7 +3,7 @@ import { useState } from 'react' import Textarea from '.' const meta = { - title: 'Base/Textarea', + title: 'Base/Input/Textarea', component: Textarea, parameters: { layout: 'centered', diff --git a/web/app/components/base/voice-input/index.stories.tsx b/web/app/components/base/voice-input/index.stories.tsx index 8d92f587c4..0a7980e9ac 100644 --- a/web/app/components/base/voice-input/index.stories.tsx +++ b/web/app/components/base/voice-input/index.stories.tsx @@ -81,7 +81,7 @@ const VoiceInputMock = ({ onConverted, onCancel }: any) => { } const meta = { - title: 'Base/VoiceInput', + title: 'Base/Input/VoiceInput', component: VoiceInputMock, parameters: { layout: 'centered', diff --git a/web/app/components/base/with-input-validation/index.stories.tsx b/web/app/components/base/with-input-validation/index.stories.tsx index 98d2d0bafb..5a7e4bc678 100644 --- a/web/app/components/base/with-input-validation/index.stories.tsx +++ b/web/app/components/base/with-input-validation/index.stories.tsx @@ -63,7 +63,7 @@ const ValidatedUserCard = withValidation(UserCard, userSchema) const ValidatedProductCard = withValidation(ProductCard, productSchema) const meta = { - title: 'Base/WithInputValidation', + title: 'Base/Input/WithInputValidation', parameters: { layout: 'centered', docs: {