diff --git a/web/.eslintrc.js b/web/.eslintrc.js index 6fcb22cb8..a82c38a29 100644 --- a/web/.eslintrc.js +++ b/web/.eslintrc.js @@ -13,7 +13,7 @@ module.exports = { 'check-file/filename-naming-convention': [ 'error', { - '**/*.{jsx,tsx}': 'KEBAB_CASE', + '**/*.{jsx,tsx}': '[a-z0-9.-]*', '**/*.{js,ts}': '[a-z0-9.-]*', }, ], diff --git a/web/.storybook/preview.ts b/web/.storybook/preview.ts index e2a95d75d..67dfee662 100644 --- a/web/.storybook/preview.ts +++ b/web/.storybook/preview.ts @@ -1,4 +1,7 @@ import type { Preview } from '@storybook/react-webpack5'; +import { createElement } from 'react'; +import { TooltipProvider } from '../src/components/ui/tooltip'; + import '../tailwind.css'; const preview: Preview = { @@ -10,6 +13,9 @@ const preview: Preview = { }, }, }, + decorators: [ + (Story) => createElement(TooltipProvider, null, createElement(Story)), + ], }; export default preview; diff --git a/web/src/stories/avatar-upload.stories.ts b/web/src/stories/avatar-upload.stories.ts new file mode 100644 index 000000000..e50156ace --- /dev/null +++ b/web/src/stories/avatar-upload.stories.ts @@ -0,0 +1,99 @@ +import type { Meta, StoryObj } from '@storybook/react-webpack5'; + +import { fn } from 'storybook/test'; + +import { AvatarUpload } from '@/components/avatar-upload'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Example/AvatarUpload', + component: AvatarUpload, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + layout: 'centered', + docs: { + description: { + component: ` +## AvatarUpload Component + +AvatarUpload is a file upload component specifically designed for uploading and displaying avatar images. It supports image preview, removal, and provides a user-friendly interface for avatar management. + +### Import Path +\`\`\`typescript +import { AvatarUpload } from '@/components/avatar-upload'; +\`\`\` + +### Basic Usage +\`\`\`tsx +import { useState } from 'react'; +import { AvatarUpload } from '@/components/avatar-upload'; + +function MyComponent() { + const [avatarValue, setAvatarValue] = useState(''); + + return ( + setAvatarValue(base64String)} + /> + ); +} +\`\`\` + +### Features +- Supports image upload with drag & drop +- Image preview with hover effects +- Remove button to clear selected image +- Base64 encoding for easy handling +- Accepts common image formats (jpg, jpeg, png, webp, bmp) + `, + }, + }, + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + value: { + description: 'The current avatar value as base64 string', + control: { type: 'text' }, + type: { name: 'string', required: false }, + }, + onChange: { + description: 'Callback function called when avatar changes', + control: false, + type: { name: 'function', required: false }, + }, + }, + // Use `fn` to spy on the onChange arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + args: { onChange: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const EmptyState: Story = { + args: { + value: '', + }, + parameters: { + docs: { + description: { + story: ` +### Empty State + +Shows the upload area when no avatar is selected. + +\`\`\`tsx + console.log('Avatar uploaded:', base64String)} +/> +\`\`\` + `, + }, + }, + }, + tags: ['!dev'], +}; diff --git a/web/src/stories/ragflow-avatar.stories.ts b/web/src/stories/ragflow-avatar.stories.ts new file mode 100644 index 000000000..7089dc7ac --- /dev/null +++ b/web/src/stories/ragflow-avatar.stories.ts @@ -0,0 +1,192 @@ +import type { Meta, StoryObj } from '@storybook/react-webpack5'; + +import { RAGFlowAvatar } from '@/components/ragflow-avatar'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Example/RAGFlowAvatar', + component: RAGFlowAvatar, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + layout: 'centered', + docs: { + description: { + component: ` +## RAGFlowAvatar Component + +RAGFlowAvatar is a customizable avatar component that displays user avatars with intelligent fallbacks. When an image is not available, it generates colorful gradient backgrounds with user initials. + +### Import Path +\`\`\`typescript +import { RAGFlowAvatar } from '@/components/ragflow-avatar'; +\`\`\` + +### Basic Usage +\`\`\`tsx +import { RAGFlowAvatar } from '@/components/ragflow-avatar'; + +function MyComponent() { + return ( + + ); +} +\`\`\` + +### Features +- Displays user avatar images when available +- Generates colorful gradient fallbacks with initials +- Supports both person (circular) and non-person (rounded) styles +- Automatic font size calculation based on container size +- Color generation based on name for consistent appearance +- Responsive design with resize observer + `, + }, + }, + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + name: { + description: + 'The name to display initials for when no avatar is available', + control: { type: 'text' }, + type: { name: 'string', required: false }, + }, + avatar: { + description: 'The URL of the avatar image', + control: { type: 'text' }, + type: { name: 'string', required: false }, + }, + isPerson: { + description: 'Whether this avatar represents a person (affects styling)', + control: { type: 'boolean' }, + type: { name: 'boolean', required: false }, + defaultValue: false, + }, + className: { + description: 'Additional CSS classes to apply', + control: { type: 'text' }, + type: { name: 'string', required: false }, + }, + }, + args: { + name: 'John Doe', + isPerson: false, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const WithInitials: Story = { + args: { + name: 'John Doe', + isPerson: false, + }, + parameters: { + docs: { + description: { + story: ` +### With Initials Only + +Shows the avatar component with only a name, displaying generated initials with a gradient background. + +\`\`\`tsx + +\`\`\` + `, + }, + }, + }, + tags: ['!dev'], +}; + +export const WithAvatar: Story = { + args: { + name: 'Jane Smith', + avatar: + 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjQiIGhlaWdodD0iNjQiIHZpZXdCb3g9IjAgMCA2NCA2NCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjY0IiBoZWlnaHQ9IjY0IiByeD0iMzIiIGZpbGw9IiM0RjZERUUiLz4KPGNpcmNsZSBjeD0iMzIiIGN5PSIyNCIgcj0iOCIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTQ4IDQ4QzQ4IDQxLjM3MyA0MS45NDA2IDM2IDM0LjUgMzZIMjkuNUMyMi4wNTk0IDM2IDE2IDQxLjM3MyAxNiA0OCIgZmlsbD0id2hpdGUiLz4KPC9zdmc+', + isPerson: true, + }, + parameters: { + docs: { + description: { + story: ` +### With Avatar Image + +Shows the avatar component with an actual image. When isPerson is true, the avatar will be circular. + +\`\`\`tsx + +\`\`\` + `, + }, + }, + }, + tags: ['!dev'], +}; + +export const PersonStyle: Story = { + args: { + name: 'Alice Johnson', + isPerson: true, + }, + parameters: { + docs: { + description: { + story: ` +### Person Style (Circular) + +Shows the avatar component with isPerson set to true, which makes it circular. + +\`\`\`tsx + +\`\`\` + `, + }, + }, + }, + tags: ['!dev'], +}; + +export const NonPersonStyle: Story = { + args: { + name: 'Bot Assistant', + isPerson: false, + }, + parameters: { + docs: { + description: { + story: ` +### Non-Person Style (Rounded Rectangle) + +Shows the avatar component with isPerson set to false, which makes it a rounded rectangle. + +\`\`\`tsx + +\`\`\` + `, + }, + }, + }, + tags: ['!dev'], +}; diff --git a/web/src/stories/ragflow-form.stories.tsx b/web/src/stories/ragflow-form.stories.tsx new file mode 100644 index 000000000..de1bb4034 --- /dev/null +++ b/web/src/stories/ragflow-form.stories.tsx @@ -0,0 +1,231 @@ +import { zodResolver } from '@hookform/resolvers/zod'; +import type { Meta, StoryObj } from '@storybook/react-webpack5'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; + +import { RAGFlowFormItem } from '@/components/ragflow-form'; +import { Form } from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; + +// Define form schema +const FormSchema = z.object({ + username: z.string().min(2, { + message: 'Username must be at least 2 characters.', + }), + email: z.string().email({ + message: 'Please enter a valid email address.', + }), + description: z.string().optional(), +}); + +// Create a wrapper component to demonstrate RAGFlowFormItem +function FormExample({ + horizontal = false, + fieldName = 'username', + label = 'Username', + tooltip = 'Please enter your username', + placeholder = 'Enter username', +}: { + horizontal?: boolean; + fieldName?: string; + label?: string; + tooltip?: string; + placeholder?: string; +}) { + const form = useForm({ + resolver: zodResolver(FormSchema), + defaultValues: { + username: '', + email: '', + description: '', + }, + }); + + return ( +
+
+ + + + +
+ +
+ ); +} + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Example/RAGFlowForm', + component: FormExample, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + layout: 'centered', + docs: { + description: { + component: ` +## RAGFlowFormItem Component + +RAGFlowFormItem is a wrapper component built on top of shadcn/ui Form components, providing unified form item styling and layout. + +### Import Path +\`\`\`typescript +import { RAGFlowFormItem } from '@/components/ragflow-form'; +import { Form } from '@/components/ui/form'; +\`\`\` + +### Basic Usage +\`\`\`tsx +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; + +const FormSchema = z.object({ + username: z.string(), +}); + +function MyForm() { + const form = useForm({ + resolver: zodResolver(FormSchema), + defaultValues: { username: '' }, + }); + + return ( +
+ + + + +
+ + ); +} +\`\`\` + +### Features +- Built-in FormField, FormItem, FormLabel, FormControl and FormMessage +- Supports both horizontal and vertical layouts +- Supports tooltip hints +- Fully compatible with react-hook-form + `, + }, + }, + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + horizontal: { + description: 'Whether to display the form item horizontally', + control: { type: 'boolean' }, + type: { name: 'boolean', required: false }, + defaultValue: false, + }, + fieldName: { + description: 'The name of the form field', + control: { type: 'text' }, + type: { name: 'string', required: true }, + }, + label: { + description: 'The label of the form field', + control: { type: 'text' }, + type: { name: 'string', required: false }, + }, + tooltip: { + description: 'The tooltip text for the form field', + control: { type: 'text' }, + type: { name: 'string', required: false }, + }, + placeholder: { + description: 'The placeholder text for the input', + control: { type: 'text' }, + type: { name: 'string', required: false }, + }, + }, + args: { + horizontal: false, + fieldName: 'username', + label: 'Username', + tooltip: 'Please enter your username', + placeholder: 'Enter username', + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const VerticalLayout: Story = { + args: { + horizontal: false, + fieldName: 'username', + label: 'Username', + tooltip: 'Please enter your username', + placeholder: 'Enter username', + }, + parameters: { + docs: { + description: { + story: ` +### Vertical Layout Example + +Default vertical layout with label above the input field. + +\`\`\`tsx + + + +\`\`\` + `, + }, + }, + }, + // tags: ['!dev'], +}; + +export const HorizontalLayout: Story = { + args: { + horizontal: true, + fieldName: 'email', + label: 'Email Address', + tooltip: 'Please enter a valid email address', + placeholder: 'Enter email', + }, + parameters: { + docs: { + description: { + story: ` +### Horizontal Layout Example + +Horizontal layout with label and input field on the same row. + +\`\`\`tsx + + + +\`\`\` + `, + }, + }, + }, + // tags: ['!dev'], +}; diff --git a/web/src/stories/ragflow-pagination.stories.ts b/web/src/stories/ragflow-pagination.stories.ts new file mode 100644 index 000000000..8451456ce --- /dev/null +++ b/web/src/stories/ragflow-pagination.stories.ts @@ -0,0 +1,68 @@ +import type { Meta, StoryObj } from '@storybook/react-webpack5'; + +import { fn } from 'storybook/test'; + +import { RAGFlowPagination } from '@/components/ui/ragflow-pagination'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Example/RAGFlowPagination', + component: RAGFlowPagination, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + layout: 'centered', + docs: { + description: { + component: ` +## Component Description + +RAGFlowPagination is a pagination component that helps navigate through large datasets by dividing them into pages. It provides intuitive controls for users to move between pages, adjust page size, and view their current position within the dataset.`, + }, + }, + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + current: { control: 'number' }, + pageSize: { control: 'number' }, + total: { control: 'number' }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + args: { onChange: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const WithLoading: Story = { + args: { + current: 1, + pageSize: 10, + total: 100, + showSizeChanger: true, + }, + parameters: { + docs: { + description: { + story: ` +### Usage Examples + +\`\`\`tsx +import { RAGFlowPagination } from '@/components/ui/ragflow-pagination'; + + { + setPagination({ page, pageSize }); + }}> + +\`\`\` + `, + }, + }, + }, + tags: ['!dev'], +}; diff --git a/web/src/stories/skeleton-card.stories.ts b/web/src/stories/skeleton-card.stories.ts new file mode 100644 index 000000000..55a90966c --- /dev/null +++ b/web/src/stories/skeleton-card.stories.ts @@ -0,0 +1,85 @@ +import type { Meta, StoryObj } from '@storybook/react-webpack5'; + +import { SkeletonCard } from '@/components/skeleton-card'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Example/SkeletonCard', + component: SkeletonCard, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + layout: 'centered', + docs: { + description: { + component: ` +## SkeletonCard Component + +SkeletonCard is a loading placeholder component that displays skeleton lines while content is being loaded. It provides a consistent loading experience with animated placeholders. + +### Import Path +\`\`\`typescript +import { SkeletonCard } from '@/components/skeleton-card'; +\`\`\` + +### Basic Usage +\`\`\`tsx +import { SkeletonCard } from '@/components/skeleton-card'; + +function MyComponent() { + return ( + + ); +} +\`\`\` + +### Features +- Displays animated skeleton loading placeholders +- Three lines of skeleton content with varying widths +- Customizable styling through className prop +- Consistent spacing and appearance +- Built on top of the Skeleton UI component + `, + }, + }, + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + className: { + description: 'Additional CSS classes to apply to the skeleton card', + control: { type: 'text' }, + type: { name: 'string', required: false }, + }, + }, + args: { + className: '', + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args + +export const WithCustomWidth: Story = { + args: { + className: 'w-80', + }, + parameters: { + docs: { + description: { + story: ` +### Custom Width + +Shows the skeleton card with a custom width applied. + +\`\`\`tsx + +\`\`\` + `, + }, + }, + }, + tags: ['!dev'], +};