diff --git a/web/.storybook/__mocks__/context-block.tsx b/web/.storybook/__mocks__/context-block.tsx new file mode 100644 index 0000000000..8a9d8625cc --- /dev/null +++ b/web/.storybook/__mocks__/context-block.tsx @@ -0,0 +1,4 @@ +// Mock for context-block plugin to avoid circular dependency in Storybook +export const ContextBlockNode = null +export const ContextBlockReplacementBlock = null +export default null diff --git a/web/.storybook/__mocks__/history-block.tsx b/web/.storybook/__mocks__/history-block.tsx new file mode 100644 index 0000000000..e3c3965d13 --- /dev/null +++ b/web/.storybook/__mocks__/history-block.tsx @@ -0,0 +1,4 @@ +// Mock for history-block plugin to avoid circular dependency in Storybook +export const HistoryBlockNode = null +export const HistoryBlockReplacementBlock = null +export default null diff --git a/web/.storybook/__mocks__/query-block.tsx b/web/.storybook/__mocks__/query-block.tsx new file mode 100644 index 0000000000..d82f51363a --- /dev/null +++ b/web/.storybook/__mocks__/query-block.tsx @@ -0,0 +1,4 @@ +// Mock for query-block plugin to avoid circular dependency in Storybook +export const QueryBlockNode = null +export const QueryBlockReplacementBlock = null +export default null diff --git a/web/.storybook/main.ts b/web/.storybook/main.ts index 0605c71346..e656115ceb 100644 --- a/web/.storybook/main.ts +++ b/web/.storybook/main.ts @@ -1,4 +1,9 @@ import type { StorybookConfig } from '@storybook/nextjs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) const config: StorybookConfig = { stories: ['../app/components/**/*.stories.@(js|jsx|mjs|ts|tsx)'], @@ -25,5 +30,17 @@ const config: StorybookConfig = { docs: { defaultName: 'Documentation', }, + webpackFinal: async (config) => { + // Add alias to mock problematic modules with circular dependencies + config.resolve = config.resolve || {} + 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'), + } + return config + }, } export default config diff --git a/web/app/components/base/action-button/index.stories.tsx b/web/app/components/base/action-button/index.stories.tsx new file mode 100644 index 0000000000..c174adbc73 --- /dev/null +++ b/web/app/components/base/action-button/index.stories.tsx @@ -0,0 +1,262 @@ +import type { Meta, StoryObj } from '@storybook/nextjs' +import { RiAddLine, RiDeleteBinLine, RiEditLine, RiMore2Fill, RiSaveLine, RiShareLine } from '@remixicon/react' +import ActionButton, { ActionButtonState } from '.' + +const meta = { + title: 'Base/ActionButton', + component: ActionButton, + parameters: { + layout: 'centered', + docs: { + description: { + component: 'Action button component with multiple sizes and states. Commonly used for toolbar actions and inline operations.', + }, + }, + }, + tags: ['autodocs'], + argTypes: { + size: { + control: 'select', + options: ['xs', 'm', 'l', 'xl'], + description: 'Button size', + }, + state: { + control: 'select', + options: [ + ActionButtonState.Default, + ActionButtonState.Active, + ActionButtonState.Disabled, + ActionButtonState.Destructive, + ActionButtonState.Hover, + ], + description: 'Button state', + }, + children: { + control: 'text', + description: 'Button content', + }, + disabled: { + control: 'boolean', + description: 'Native disabled state', + }, + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +// Default state +export const Default: Story = { + args: { + size: 'm', + children: , + }, +} + +// With text +export const WithText: Story = { + args: { + size: 'm', + children: 'Edit', + }, +} + +// Icon with text +export const IconWithText: Story = { + args: { + size: 'm', + children: ( + <> + + Add Item + + ), + }, +} + +// Size variations +export const ExtraSmall: Story = { + args: { + size: 'xs', + children: , + }, +} + +export const Small: Story = { + args: { + size: 'xs', + children: , + }, +} + +export const Medium: Story = { + args: { + size: 'm', + children: , + }, +} + +export const Large: Story = { + args: { + size: 'l', + children: , + }, +} + +export const ExtraLarge: Story = { + args: { + size: 'xl', + children: , + }, +} + +// State variations +export const ActiveState: Story = { + args: { + size: 'm', + state: ActionButtonState.Active, + children: , + }, +} + +export const DisabledState: Story = { + args: { + size: 'm', + state: ActionButtonState.Disabled, + children: , + }, +} + +export const DestructiveState: Story = { + args: { + size: 'm', + state: ActionButtonState.Destructive, + children: , + }, +} + +export const HoverState: Story = { + args: { + size: 'm', + state: ActionButtonState.Hover, + children: , + }, +} + +// Real-world examples +export const ToolbarActions: Story = { + render: () => ( +
+ + + + + + + + + +
+ + + +
+ ), +} + +export const InlineActions: Story = { + render: () => ( +
+ Item name + + + + + + +
+ ), +} + +export const SizeComparison: Story = { + render: () => ( +
+
+ + + + XS +
+
+ + + + S +
+
+ + + + M +
+
+ + + + L +
+
+ + + + XL +
+
+ ), +} + +export const StateComparison: Story = { + render: () => ( +
+
+ + + + Default +
+
+ + + + Active +
+
+ + + + Hover +
+
+ + + + Disabled +
+
+ + + + Destructive +
+
+ ), +} + +// Interactive playground +export const Playground: Story = { + args: { + size: 'm', + state: ActionButtonState.Default, + children: , + }, +} diff --git a/web/app/components/base/auto-height-textarea/index.stories.tsx b/web/app/components/base/auto-height-textarea/index.stories.tsx new file mode 100644 index 0000000000..f083e4f56d --- /dev/null +++ b/web/app/components/base/auto-height-textarea/index.stories.tsx @@ -0,0 +1,204 @@ +import type { Meta, StoryObj } from '@storybook/nextjs' +import { useState } from 'react' +import AutoHeightTextarea from '.' + +const meta = { + title: 'Base/AutoHeightTextarea', + component: AutoHeightTextarea, + parameters: { + layout: 'centered', + docs: { + description: { + component: 'Auto-resizing textarea component that expands and contracts based on content, with configurable min/max height constraints.', + }, + }, + }, + tags: ['autodocs'], + argTypes: { + placeholder: { + control: 'text', + description: 'Placeholder text', + }, + value: { + control: 'text', + description: 'Textarea value', + }, + minHeight: { + control: 'number', + description: 'Minimum height in pixels', + }, + maxHeight: { + control: 'number', + description: 'Maximum height in pixels', + }, + autoFocus: { + control: 'boolean', + description: 'Auto focus on mount', + }, + className: { + control: 'text', + description: 'Additional CSS classes', + }, + wrapperClassName: { + control: 'text', + description: 'Wrapper CSS classes', + }, + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +// Interactive demo wrapper +const AutoHeightTextareaDemo = (args: any) => { + const [value, setValue] = useState(args.value || '') + + return ( +
+ { + setValue(e.target.value) + console.log('Text changed:', e.target.value) + }} + /> +
+ ) +} + +// Default state +export const Default: Story = { + render: args => , + args: { + placeholder: 'Type something...', + value: '', + minHeight: 36, + maxHeight: 96, + className: 'w-full p-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500', + }, +} + +// With initial value +export const WithInitialValue: Story = { + render: args => , + args: { + placeholder: 'Type something...', + value: 'This is a pre-filled textarea with some initial content.', + minHeight: 36, + maxHeight: 96, + className: 'w-full p-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500', + }, +} + +// With multiline content +export const MultilineContent: Story = { + render: args => , + args: { + placeholder: 'Type something...', + value: 'Line 1\nLine 2\nLine 3\nLine 4\nThis textarea automatically expands to fit the content.', + minHeight: 36, + maxHeight: 96, + className: 'w-full p-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500', + }, +} + +// Custom min height +export const CustomMinHeight: Story = { + render: args => , + args: { + placeholder: 'Taller minimum height...', + value: '', + minHeight: 100, + maxHeight: 200, + className: 'w-full p-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500', + }, +} + +// Small max height (scrollable) +export const SmallMaxHeight: Story = { + render: args => , + args: { + placeholder: 'Type multiple lines...', + value: 'Line 1\nLine 2\nLine 3\nLine 4\nLine 5\nLine 6\nThis will become scrollable when it exceeds max height.', + minHeight: 36, + maxHeight: 80, + className: 'w-full p-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500', + }, +} + +// Auto focus enabled +export const AutoFocus: Story = { + render: args => , + args: { + placeholder: 'This textarea auto-focuses on mount', + value: '', + minHeight: 36, + maxHeight: 96, + autoFocus: true, + className: 'w-full p-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500', + }, +} + +// With custom styling +export const CustomStyling: Story = { + render: args => , + args: { + placeholder: 'Custom styled textarea...', + value: '', + minHeight: 50, + maxHeight: 150, + className: 'w-full p-3 bg-gray-50 border-2 border-blue-400 rounded-xl text-lg focus:outline-none focus:bg-white focus:border-blue-600', + wrapperClassName: 'shadow-lg', + }, +} + +// Long content example +export const LongContent: Story = { + render: args => , + args: { + placeholder: 'Type something...', + value: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n\nUt enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\n\nDuis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\n\nExcepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', + minHeight: 36, + maxHeight: 200, + className: 'w-full p-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500', + }, +} + +// Real-world example - Chat input +export const ChatInput: Story = { + render: args => , + args: { + placeholder: 'Type your message...', + value: '', + minHeight: 40, + maxHeight: 120, + className: 'w-full px-4 py-2 bg-gray-100 border border-gray-300 rounded-2xl text-sm focus:outline-none focus:bg-white focus:ring-2 focus:ring-blue-500', + }, +} + +// Real-world example - Comment box +export const CommentBox: Story = { + render: args => , + args: { + placeholder: 'Write a comment...', + value: '', + minHeight: 60, + maxHeight: 200, + className: 'w-full p-3 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500', + }, +} + +// Interactive playground +export const Playground: Story = { + render: args => , + args: { + placeholder: 'Type something...', + value: '', + minHeight: 36, + maxHeight: 96, + autoFocus: false, + className: 'w-full p-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500', + wrapperClassName: '', + }, +} diff --git a/web/app/components/base/block-input/index.stories.tsx b/web/app/components/base/block-input/index.stories.tsx new file mode 100644 index 0000000000..0685f4150f --- /dev/null +++ b/web/app/components/base/block-input/index.stories.tsx @@ -0,0 +1,191 @@ +import type { Meta, StoryObj } from '@storybook/nextjs' +import { useState } from 'react' +import BlockInput from '.' + +const meta = { + title: 'Base/BlockInput', + component: BlockInput, + parameters: { + layout: 'centered', + docs: { + description: { + component: 'Block input component with variable highlighting. Supports {{variable}} syntax with validation and visual highlighting of variable names.', + }, + }, + }, + tags: ['autodocs'], + argTypes: { + value: { + control: 'text', + description: 'Input value (supports {{variable}} syntax)', + }, + className: { + control: 'text', + description: 'Wrapper CSS classes', + }, + highLightClassName: { + control: 'text', + description: 'CSS class for highlighted variables (default: text-blue-500)', + }, + readonly: { + control: 'boolean', + description: 'Read-only mode', + }, + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +// Interactive demo wrapper +const BlockInputDemo = (args: any) => { + const [value, setValue] = useState(args.value || '') + const [keys, setKeys] = useState([]) + + return ( +
+ { + setValue(newValue) + setKeys(extractedKeys) + console.log('Value confirmed:', newValue) + console.log('Extracted keys:', extractedKeys) + }} + /> + {keys.length > 0 && ( +
+
Detected Variables:
+
+ {keys.map(key => ( + + {key} + + ))} +
+
+ )} +
+ ) +} + +// Default state +export const Default: Story = { + render: args => , + args: { + value: '', + readonly: false, + }, +} + +// With single variable +export const SingleVariable: Story = { + render: args => , + args: { + value: 'Hello {{name}}, welcome to the application!', + readonly: false, + }, +} + +// With multiple variables +export const MultipleVariables: Story = { + render: args => , + args: { + value: 'Dear {{user_name}},\n\nYour order {{order_id}} has been shipped to {{address}}.\n\nThank you for shopping with us!', + readonly: false, + }, +} + +// Complex template +export const ComplexTemplate: Story = { + render: args => , + args: { + value: 'Hi {{customer_name}},\n\nYour {{product_type}} subscription will renew on {{renewal_date}} for {{amount}}.\n\nYour payment method ending in {{card_last_4}} will be charged.\n\nQuestions? Contact us at {{support_email}}.', + readonly: false, + }, +} + +// Read-only mode +export const ReadOnlyMode: Story = { + render: args => , + args: { + value: 'This is a read-only template with {{variable1}} and {{variable2}}.\n\nYou cannot edit this content.', + readonly: true, + }, +} + +// Empty state +export const EmptyState: Story = { + render: args => , + args: { + value: '', + readonly: false, + }, +} + +// Long content +export const LongContent: Story = { + render: args => , + args: { + value: 'Dear {{recipient_name}},\n\nWe are writing to inform you about the upcoming changes to your {{service_name}} account.\n\nEffective {{effective_date}}, your plan will include:\n\n1. Access to {{feature_1}}\n2. {{feature_2}} with unlimited usage\n3. Priority support via {{support_channel}}\n4. Monthly reports sent to {{email_address}}\n\nYour new monthly rate will be {{new_price}}, compared to your current rate of {{old_price}}.\n\nIf you have any questions, please contact our team at {{contact_info}}.\n\nBest regards,\n{{company_name}} Team', + readonly: false, + }, +} + +// Variables with underscores +export const VariablesWithUnderscores: Story = { + render: args => , + args: { + value: 'User {{user_id}} from {{user_country}} has {{total_orders}} orders with status {{order_status}}.', + readonly: false, + }, +} + +// Adjacent variables +export const AdjacentVariables: Story = { + render: args => , + args: { + value: 'File: {{file_name}}.{{file_extension}} ({{file_size}}{{size_unit}})', + readonly: false, + }, +} + +// Real-world example - Email template +export const EmailTemplate: Story = { + render: args => , + args: { + value: 'Subject: Your {{service_name}} account has been created\n\nHi {{first_name}},\n\nWelcome to {{company_name}}! Your account is now active.\n\nUsername: {{username}}\nEmail: {{email}}\n\nGet started at {{app_url}}\n\nThanks,\nThe {{company_name}} Team', + readonly: false, + }, +} + +// Real-world example - Notification template +export const NotificationTemplate: Story = { + render: args => , + args: { + value: '🔔 {{user_name}} mentioned you in {{channel_name}}\n\n"{{message_preview}}"\n\nReply now: {{message_url}}', + readonly: false, + }, +} + +// Custom styling +export const CustomStyling: Story = { + render: args => , + args: { + value: 'This template uses {{custom_variable}} with custom styling.', + readonly: false, + className: 'bg-gray-50 border-2 border-blue-200', + }, +} + +// Interactive playground +export const Playground: Story = { + render: args => , + args: { + value: 'Try editing this text and adding variables like {{example}}', + readonly: false, + className: '', + highLightClassName: '', + }, +} 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 1f45844ec4..a83c0fea61 100644 --- a/web/app/components/base/chat/chat/answer/index.stories.tsx +++ b/web/app/components/base/chat/chat/answer/index.stories.tsx @@ -1,7 +1,5 @@ import type { Meta, StoryObj } from '@storybook/nextjs' - import type { ChatItem } from '../../types' -import { mockedWorkflowProcess } from './__mocks__/workflowProcess' import { markdownContent } from './__mocks__/markdownContent' import { markdownContentSVG } from './__mocks__/markdownContentSVG' import Answer from '.' @@ -34,6 +32,11 @@ const mockedBaseChatItem = { content: 'Hello, how can I assist you today?', } satisfies ChatItem +const mockedWorkflowProcess = { + status: 'succeeded', + tracing: [], +} + export const Basic: Story = { args: { item: mockedBaseChatItem, diff --git a/web/app/components/base/checkbox/index.stories.tsx b/web/app/components/base/checkbox/index.stories.tsx new file mode 100644 index 0000000000..65fa8e1b97 --- /dev/null +++ b/web/app/components/base/checkbox/index.stories.tsx @@ -0,0 +1,394 @@ +import type { Meta, StoryObj } from '@storybook/nextjs' +import { useState } from 'react' +import Checkbox from '.' + +// Helper function for toggling items in an array +const createToggleItem = ( + items: T[], + setItems: (items: T[]) => void, +) => (id: string) => { + setItems(items.map(item => + item.id === id ? { ...item, checked: !item.checked } as T : item, + )) +} + +const meta = { + title: 'Base/Checkbox', + component: Checkbox, + parameters: { + layout: 'centered', + docs: { + description: { + component: 'Checkbox component with support for checked, unchecked, indeterminate, and disabled states.', + }, + }, + }, + tags: ['autodocs'], + argTypes: { + checked: { + control: 'boolean', + description: 'Checked state', + }, + indeterminate: { + control: 'boolean', + description: 'Indeterminate state (partially checked)', + }, + disabled: { + control: 'boolean', + description: 'Disabled state', + }, + className: { + control: 'text', + description: 'Additional CSS classes', + }, + id: { + control: 'text', + description: 'HTML id attribute', + }, + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +// Interactive demo wrapper +const CheckboxDemo = (args: any) => { + const [checked, setChecked] = useState(args.checked || false) + + return ( +
+ { + if (!args.disabled) { + setChecked(!checked) + console.log('Checkbox toggled:', !checked) + } + }} + /> + + {checked ? 'Checked' : 'Unchecked'} + +
+ ) +} + +// Default unchecked +export const Default: Story = { + render: args => , + args: { + checked: false, + disabled: false, + indeterminate: false, + }, +} + +// Checked state +export const Checked: Story = { + render: args => , + args: { + checked: true, + disabled: false, + indeterminate: false, + }, +} + +// Indeterminate state +export const Indeterminate: Story = { + render: args => , + args: { + checked: false, + disabled: false, + indeterminate: true, + }, +} + +// Disabled unchecked +export const DisabledUnchecked: Story = { + render: args => , + args: { + checked: false, + disabled: true, + indeterminate: false, + }, +} + +// Disabled checked +export const DisabledChecked: Story = { + render: args => , + args: { + checked: true, + disabled: true, + indeterminate: false, + }, +} + +// Disabled indeterminate +export const DisabledIndeterminate: Story = { + render: args => , + args: { + checked: false, + disabled: true, + indeterminate: true, + }, +} + +// State comparison +export const StateComparison: Story = { + render: () => ( +
+
+
+ undefined} /> + Unchecked +
+
+ undefined} /> + Checked +
+
+ undefined} /> + Indeterminate +
+
+
+
+ undefined} /> + Disabled +
+
+ undefined} /> + Disabled Checked +
+
+ undefined} /> + Disabled Indeterminate +
+
+
+ ), +} + +// With labels +const WithLabelsDemo = () => { + const [items, setItems] = useState([ + { id: '1', label: 'Enable notifications', checked: true }, + { id: '2', label: 'Enable email updates', checked: false }, + { id: '3', label: 'Enable SMS alerts', checked: false }, + ]) + + const toggleItem = createToggleItem(items, setItems) + + return ( +
+ {items.map(item => ( +
+ toggleItem(item.id)} + /> + +
+ ))} +
+ ) +} + +export const WithLabels: Story = { + render: () => , +} + +// Select all example +const SelectAllExampleDemo = () => { + const [items, setItems] = useState([ + { id: '1', label: 'Item 1', checked: false }, + { id: '2', label: 'Item 2', checked: false }, + { id: '3', label: 'Item 3', checked: false }, + ]) + + const allChecked = items.every(item => item.checked) + const someChecked = items.some(item => item.checked) + const indeterminate = someChecked && !allChecked + + const toggleAll = () => { + const newChecked = !allChecked + setItems(items.map(item => ({ ...item, checked: newChecked }))) + } + + const toggleItem = createToggleItem(items, setItems) + + return ( +
+
+ + Select All +
+
+ {items.map(item => ( +
+ toggleItem(item.id)} + /> + +
+ ))} +
+
+ ) +} + +export const SelectAllExample: Story = { + render: () => , +} + +// Form example +const FormExampleDemo = () => { + const [formData, setFormData] = useState({ + terms: false, + newsletter: false, + privacy: false, + }) + + return ( +
+

Account Settings

+
+
+ setFormData({ ...formData, terms: !formData.terms })} + /> +
+ +

+ Required to continue +

+
+
+
+ setFormData({ ...formData, newsletter: !formData.newsletter })} + /> +
+ +

+ Get updates about new features +

+
+
+
+ setFormData({ ...formData, privacy: !formData.privacy })} + /> +
+ +

+ Required to continue +

+
+
+
+
+ ) +} + +export const FormExample: Story = { + render: () => , +} + +// Task list example +const TaskListExampleDemo = () => { + const [tasks, setTasks] = useState([ + { id: '1', title: 'Review pull request', completed: true }, + { id: '2', title: 'Update documentation', completed: true }, + { id: '3', title: 'Fix navigation bug', completed: false }, + { id: '4', title: 'Deploy to staging', completed: false }, + ]) + + const toggleTask = (id: string) => { + setTasks(tasks.map(task => + task.id === id ? { ...task, completed: !task.completed } : task, + )) + } + + const completedCount = tasks.filter(t => t.completed).length + + return ( +
+
+

Today's Tasks

+ + {completedCount} of {tasks.length} completed + +
+
+ {tasks.map(task => ( +
+ toggleTask(task.id)} + /> + toggleTask(task.id)} + > + {task.title} + +
+ ))} +
+
+ ) +} + +export const TaskListExample: Story = { + render: () => , +} + +// Interactive playground +export const Playground: Story = { + render: args => , + args: { + checked: false, + indeterminate: false, + disabled: false, + id: 'playground-checkbox', + }, +} diff --git a/web/app/components/base/input-number/index.stories.tsx b/web/app/components/base/input-number/index.stories.tsx new file mode 100644 index 0000000000..0fca2e52f9 --- /dev/null +++ b/web/app/components/base/input-number/index.stories.tsx @@ -0,0 +1,438 @@ +import type { Meta, StoryObj } from '@storybook/nextjs' +import { useState } from 'react' +import { InputNumber } from '.' + +const meta = { + title: 'Base/InputNumber', + component: InputNumber, + parameters: { + layout: 'centered', + docs: { + description: { + component: 'Number input component with increment/decrement buttons. Supports min/max constraints, custom step amounts, and units display.', + }, + }, + }, + tags: ['autodocs'], + argTypes: { + value: { + control: 'number', + description: 'Current value', + }, + size: { + control: 'select', + options: ['regular', 'large'], + description: 'Input size', + }, + min: { + control: 'number', + description: 'Minimum value', + }, + max: { + control: 'number', + description: 'Maximum value', + }, + amount: { + control: 'number', + description: 'Step amount for increment/decrement', + }, + unit: { + control: 'text', + description: 'Unit text displayed (e.g., "px", "ms")', + }, + disabled: { + control: 'boolean', + description: 'Disabled state', + }, + defaultValue: { + control: 'number', + description: 'Default value when undefined', + }, + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +// Interactive demo wrapper +const InputNumberDemo = (args: any) => { + const [value, setValue] = useState(args.value ?? 0) + + return ( +
+ { + setValue(newValue) + console.log('Value changed:', newValue) + }} + /> +
+ Current value: {value} +
+
+ ) +} + +// Default state +export const Default: Story = { + render: args => , + args: { + value: 0, + size: 'regular', + }, +} + +// Large size +export const LargeSize: Story = { + render: args => , + args: { + value: 10, + size: 'large', + }, +} + +// With min/max constraints +export const WithMinMax: Story = { + render: args => , + args: { + value: 5, + min: 0, + max: 10, + size: 'regular', + }, +} + +// With custom step amount +export const CustomStepAmount: Story = { + render: args => , + args: { + value: 50, + amount: 5, + min: 0, + max: 100, + size: 'regular', + }, +} + +// With unit +export const WithUnit: Story = { + render: args => , + args: { + value: 100, + unit: 'px', + min: 0, + max: 1000, + amount: 10, + size: 'regular', + }, +} + +// Disabled state +export const Disabled: Story = { + render: args => , + args: { + value: 42, + disabled: true, + size: 'regular', + }, +} + +// Decimal values +export const DecimalValues: Story = { + render: args => , + args: { + value: 2.5, + amount: 0.5, + min: 0, + max: 10, + size: 'regular', + }, +} + +// Negative values allowed +export const NegativeValues: Story = { + render: args => , + args: { + value: 0, + min: -100, + max: 100, + amount: 10, + size: 'regular', + }, +} + +// Size comparison +const SizeComparisonDemo = () => { + const [regularValue, setRegularValue] = useState(10) + const [largeValue, setLargeValue] = useState(20) + + return ( +
+
+ + +
+
+ + +
+
+ ) +} + +export const SizeComparison: Story = { + render: () => , +} + +// Real-world example - Font size picker +const FontSizePickerDemo = () => { + const [fontSize, setFontSize] = useState(16) + + return ( +
+
+
+ + +
+
+

+ Preview Text +

+
+
+
+ ) +} + +export const FontSizePicker: Story = { + render: () => , +} + +// Real-world example - Quantity selector +const QuantitySelectorDemo = () => { + const [quantity, setQuantity] = useState(1) + const pricePerItem = 29.99 + const total = (quantity * pricePerItem).toFixed(2) + + return ( +
+
+
+
+

Product Name

+

${pricePerItem} each

+
+
+
+ + +
+
+
+ Total + ${total} +
+
+
+
+ ) +} + +export const QuantitySelector: Story = { + render: () => , +} + +// Real-world example - Timer settings +const TimerSettingsDemo = () => { + const [hours, setHours] = useState(0) + const [minutes, setMinutes] = useState(15) + const [seconds, setSeconds] = useState(30) + + const totalSeconds = hours * 3600 + minutes * 60 + seconds + + return ( +
+

Timer Configuration

+
+
+ + +
+
+ + +
+
+ + +
+
+
+ Total duration: {totalSeconds} seconds +
+
+
+
+ ) +} + +export const TimerSettings: Story = { + render: () => , +} + +// Real-world example - Animation settings +const AnimationSettingsDemo = () => { + const [duration, setDuration] = useState(300) + const [delay, setDelay] = useState(0) + const [iterations, setIterations] = useState(1) + + return ( +
+

Animation Properties

+
+
+ + +
+
+ + +
+
+ + +
+
+
+ animation: {duration}ms {delay}ms {iterations} +
+
+
+
+ ) +} + +export const AnimationSettings: Story = { + render: () => , +} + +// Real-world example - Temperature control +const TemperatureControlDemo = () => { + const [temperature, setTemperature] = useState(20) + const fahrenheit = ((temperature * 9) / 5 + 32).toFixed(1) + + return ( +
+

Temperature Control

+
+
+ + +
+
+
+
Celsius
+
{temperature}°C
+
+
+
Fahrenheit
+
{fahrenheit}°F
+
+
+
+
+ ) +} + +export const TemperatureControl: Story = { + render: () => , +} + +// Interactive playground +export const Playground: Story = { + render: args => , + args: { + value: 10, + size: 'regular', + min: 0, + max: 100, + amount: 1, + unit: '', + disabled: false, + defaultValue: 0, + }, +} diff --git a/web/app/components/base/input/index.stories.tsx b/web/app/components/base/input/index.stories.tsx new file mode 100644 index 0000000000..cd857bc180 --- /dev/null +++ b/web/app/components/base/input/index.stories.tsx @@ -0,0 +1,424 @@ +import type { Meta, StoryObj } from '@storybook/nextjs' +import { useState } from 'react' +import Input from '.' + +const meta = { + title: 'Base/Input', + component: Input, + parameters: { + layout: 'centered', + docs: { + description: { + component: 'Input component with support for icons, clear button, validation states, and units. Includes automatic leading zero removal for number inputs.', + }, + }, + }, + tags: ['autodocs'], + argTypes: { + size: { + control: 'select', + options: ['regular', 'large'], + description: 'Input size', + }, + type: { + control: 'select', + options: ['text', 'number', 'email', 'password', 'url', 'tel'], + description: 'Input type', + }, + placeholder: { + control: 'text', + description: 'Placeholder text', + }, + disabled: { + control: 'boolean', + description: 'Disabled state', + }, + destructive: { + control: 'boolean', + description: 'Error/destructive state', + }, + showLeftIcon: { + control: 'boolean', + description: 'Show search icon on left', + }, + showClearIcon: { + control: 'boolean', + description: 'Show clear button when input has value', + }, + unit: { + control: 'text', + description: 'Unit text displayed on right (e.g., "px", "ms")', + }, + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +// Interactive demo wrapper +const InputDemo = (args: any) => { + const [value, setValue] = useState(args.value || '') + + return ( +
+ { + setValue(e.target.value) + console.log('Input changed:', e.target.value) + }} + onClear={() => { + setValue('') + console.log('Input cleared') + }} + /> +
+ ) +} + +// Default state +export const Default: Story = { + render: args => , + args: { + size: 'regular', + placeholder: 'Enter text...', + type: 'text', + }, +} + +// Large size +export const LargeSize: Story = { + render: args => , + args: { + size: 'large', + placeholder: 'Enter text...', + type: 'text', + }, +} + +// With search icon +export const WithSearchIcon: Story = { + render: args => , + args: { + size: 'regular', + showLeftIcon: true, + placeholder: 'Search...', + type: 'text', + }, +} + +// With clear button +export const WithClearButton: Story = { + render: args => , + args: { + size: 'regular', + showClearIcon: true, + value: 'Some text to clear', + placeholder: 'Type something...', + type: 'text', + }, +} + +// Search input (icon + clear) +export const SearchInput: Story = { + render: args => , + args: { + size: 'regular', + showLeftIcon: true, + showClearIcon: true, + value: '', + placeholder: 'Search...', + type: 'text', + }, +} + +// Disabled state +export const Disabled: Story = { + render: args => , + args: { + size: 'regular', + value: 'Disabled input', + disabled: true, + type: 'text', + }, +} + +// Destructive/error state +export const DestructiveState: Story = { + render: args => , + args: { + size: 'regular', + value: 'invalid@email', + destructive: true, + placeholder: 'Enter email...', + type: 'email', + }, +} + +// Number input +export const NumberInput: Story = { + render: args => , + args: { + size: 'regular', + type: 'number', + placeholder: 'Enter a number...', + value: '0', + }, +} + +// With unit +export const WithUnit: Story = { + render: args => , + args: { + size: 'regular', + type: 'number', + value: '100', + unit: 'px', + placeholder: 'Enter value...', + }, +} + +// Email input +export const EmailInput: Story = { + render: args => , + args: { + size: 'regular', + type: 'email', + placeholder: 'Enter your email...', + showClearIcon: true, + }, +} + +// Password input +export const PasswordInput: Story = { + render: args => , + args: { + size: 'regular', + type: 'password', + placeholder: 'Enter password...', + value: 'secret123', + }, +} + +// Size comparison +const SizeComparisonDemo = () => { + const [regularValue, setRegularValue] = useState('') + const [largeValue, setLargeValue] = useState('') + + return ( +
+
+ + setRegularValue(e.target.value)} + placeholder="Regular input..." + showClearIcon + onClear={() => setRegularValue('')} + /> +
+
+ + setLargeValue(e.target.value)} + placeholder="Large input..." + showClearIcon + onClear={() => setLargeValue('')} + /> +
+
+ ) +} + +export const SizeComparison: Story = { + render: () => , +} + +// State comparison +const StateComparisonDemo = () => { + const [normalValue, setNormalValue] = useState('Normal state') + const [errorValue, setErrorValue] = useState('Error state') + + return ( +
+
+ + setNormalValue(e.target.value)} + showClearIcon + onClear={() => setNormalValue('')} + /> +
+
+ + setErrorValue(e.target.value)} + destructive + /> +
+
+ + undefined} + disabled + /> +
+
+ ) +} + +export const StateComparison: Story = { + render: () => , +} + +// Form example +const FormExampleDemo = () => { + const [formData, setFormData] = useState({ + name: '', + email: '', + age: '', + website: '', + }) + const [errors, setErrors] = useState({ + email: false, + age: false, + }) + + const validateEmail = (email: string) => { + return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) + } + + return ( +
+

User Profile

+
+
+ + setFormData({ ...formData, name: e.target.value })} + placeholder="Enter your name..." + showClearIcon + onClear={() => setFormData({ ...formData, name: '' })} + /> +
+
+ + { + setFormData({ ...formData, email: e.target.value }) + setErrors({ ...errors, email: e.target.value ? !validateEmail(e.target.value) : false }) + }} + placeholder="Enter your email..." + destructive={errors.email} + showClearIcon + onClear={() => { + setFormData({ ...formData, email: '' }) + setErrors({ ...errors, email: false }) + }} + /> + {errors.email && ( + Please enter a valid email address + )} +
+
+ + { + setFormData({ ...formData, age: e.target.value }) + setErrors({ ...errors, age: e.target.value ? Number(e.target.value) < 18 : false }) + }} + placeholder="Enter your age..." + destructive={errors.age} + unit="years" + /> + {errors.age && ( + Must be 18 or older + )} +
+
+ + setFormData({ ...formData, website: e.target.value })} + placeholder="https://example.com" + showClearIcon + onClear={() => setFormData({ ...formData, website: '' })} + /> +
+
+
+ ) +} + +export const FormExample: Story = { + render: () => , +} + +// Search example +const SearchExampleDemo = () => { + const [searchQuery, setSearchQuery] = useState('') + const items = ['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry', 'Fig', 'Grape'] + const filteredItems = items.filter(item => + item.toLowerCase().includes(searchQuery.toLowerCase()), + ) + + return ( +
+ setSearchQuery(e.target.value)} + onClear={() => setSearchQuery('')} + placeholder="Search fruits..." + /> + {searchQuery && ( +
+
+ {filteredItems.length} result{filteredItems.length !== 1 ? 's' : ''} +
+
+ {filteredItems.map(item => ( +
+ {item} +
+ ))} +
+
+ )} +
+ ) +} + +export const SearchExample: Story = { + render: () => , +} + +// Interactive playground +export const Playground: Story = { + render: args => , + args: { + size: 'regular', + type: 'text', + placeholder: 'Type something...', + disabled: false, + destructive: false, + showLeftIcon: false, + showClearIcon: true, + unit: '', + }, +} diff --git a/web/app/components/base/prompt-editor/index.stories.tsx b/web/app/components/base/prompt-editor/index.stories.tsx new file mode 100644 index 0000000000..17b04e4af0 --- /dev/null +++ b/web/app/components/base/prompt-editor/index.stories.tsx @@ -0,0 +1,360 @@ +import type { Meta, StoryObj } from '@storybook/nextjs' +import { useState } from 'react' + +// Mock component to avoid complex initialization issues +const PromptEditorMock = ({ value, onChange, placeholder, editable, compact, className, wrapperClassName }: any) => { + const [content, setContent] = useState(value || '') + + const handleChange = (e: React.ChangeEvent) => { + setContent(e.target.value) + onChange?.(e.target.value) + } + + return ( +
+