chore(ui): update migration status and component files

- Updated migration status to reflect the migration of additional components, reducing the total migrated count to 34 and increasing the refactored count to 14.
- Enhanced component files by refactoring several components to improve structure and styling, including CopyButton, CustomTag, and IndicatorLight.
- Added new stories for components such as CopyButton, CustomCollapse, and DividerWithText to improve documentation and showcase usage.
- Adjusted TypeScript configuration to include story files for better type checking.
This commit is contained in:
MyPrototypeWhat 2025-09-16 17:12:06 +08:00
parent d397a43806
commit f83c3e171e
28 changed files with 3153 additions and 503 deletions

View File

@ -49,33 +49,33 @@ function MyComponent() {
## 迁移概览
- **总组件数**: 236
- **已迁移**: 46
- **已重构**: 2
- **待迁移**: 190
- **已迁移**: 34
- **已重构**: 14
- **待迁移**: 188
## 组件状态表
| Category | Component Name | Migration Status | Refactoring Status | Description |
|----------|----------------|------------------|--------------------|-------------|
| **base** | | | | 基础组件 |
| | CopyButton | ✅ | | 复制按钮 |
| | CustomTag | ✅ | | 自定义标签 |
| | DividerWithText | ✅ | | 带文本的分隔线 |
| | EmojiIcon | ✅ | | 表情图标 |
| | ErrorBoundary | ✅ | | 错误边界 (通过 props 解耦) |
| | CopyButton | ✅ | | 复制按钮 |
| | CustomTag | ✅ | | 自定义标签 |
| | DividerWithText | ✅ | | 带文本的分隔线 |
| | EmojiIcon | ✅ | | 表情图标 |
| | ErrorBoundary | ✅ | | 错误边界 (通过 props 解耦) |
| | StatusTag | ✅ | ✅ | 统一状态标签(合并了 ErrorTag、SuccessTag、WarnTag、InfoTag|
| | IndicatorLight | ✅ | | 指示灯 |
| | Spinner | ✅ | | 加载动画 |
| | TextBadge | ✅ | | 文本徽标 |
| | CustomCollapse | ✅ | | 自定义折叠面板 |
| | IndicatorLight | ✅ | | 指示灯 |
| | Spinner | ✅ | | 加载动画 |
| | TextBadge | ✅ | | 文本徽标 |
| | CustomCollapse | ✅ | | 自定义折叠面板 |
| **display** | | | | 显示组件 |
| | Ellipsis | ✅ | ❌ | 文本省略 |
| | ExpandableText | ✅ | | 可展开文本 |
| | ExpandableText | ✅ | | 可展开文本 |
| | ThinkingEffect | ✅ | ❌ | 思考效果动画 |
| | EmojiAvatar | ✅ | | 表情头像 |
| | EmojiAvatar | ✅ | | 表情头像 |
| | ListItem | ✅ | ❌ | 列表项 |
| | MaxContextCount | ✅ | ❌ | 最大上下文数显示 |
| | ProviderAvatar | ✅ | | 提供者头像 |
| | ProviderAvatar | ✅ | | 提供者头像 |
| | CodeViewer | ❌ | ❌ | 代码查看器 (外部依赖) |
| | OGCard | ❌ | ❌ | OG 卡片 |
| | MarkdownShadowDOMRenderer | ❌ | ❌ | Markdown 渲染器 |

View File

@ -48,35 +48,33 @@ When submitting PRs, please place components in the correct directory based on t
## Migration Overview
- **Total Components**: 236
- **Migrated**: 46
- **Refactored**: 0
- **Pending Migration**: 190
- **Migrated**: 34
- **Refactored**: 14
- **Pending Migration**: 188
## Component Status Table
| Category | Component Name | Migration Status | Refactoring Status | Description |
|----------|----------------|------------------|--------------------|-------------|
| **base** | | | | Base components |
| | CopyButton | ✅ | ❌ | Copy button |
| | CustomTag | ✅ | ❌ | Custom tag |
| | DividerWithText | ✅ | ❌ | Divider with text |
| | EmojiIcon | ✅ | ❌ | Emoji icon |
| | ErrorBoundary | ✅ | ❌ | Error boundary (decoupled via props) |
| | ErrorTag | ✅ | ❌ | Error tag |
| | IndicatorLight | ✅ | ❌ | Indicator light |
| | Spinner | ✅ | ❌ | Loading spinner |
| | SuccessTag | ✅ | ❌ | Success tag |
| | TextBadge | ✅ | ❌ | Text badge |
| | WarnTag | ✅ | ❌ | Warning tag |
| | CustomCollapse | ✅ | ❌ | Custom collapse panel |
| | CopyButton | ✅ | ✅ | Copy button |
| | CustomTag | ✅ | ✅ | Custom tag |
| | DividerWithText | ✅ | ✅ | Divider with text |
| | EmojiIcon | ✅ | ✅ | Emoji icon |
| | ErrorBoundary | ✅ | ✅ | Error boundary (decoupled via props) |
| | StatusTag | ✅ | ✅ | Unified status tag (merged ErrorTag, SuccessTag, WarnTag, InfoTag) |
| | IndicatorLight | ✅ | ✅ | Indicator light |
| | Spinner | ✅ | ✅ | Loading spinner |
| | TextBadge | ✅ | ✅ | Text badge |
| | CustomCollapse | ✅ | ✅ | Custom collapse panel |
| **display** | | | | Display components |
| | Ellipsis | ✅ | ❌ | Text ellipsis |
| | ExpandableText | ✅ | | Expandable text |
| | ExpandableText | ✅ | | Expandable text |
| | ThinkingEffect | ✅ | ❌ | Thinking effect animation |
| | EmojiAvatar | ✅ | | Emoji avatar |
| | EmojiAvatar | ✅ | | Emoji avatar |
| | ListItem | ✅ | ❌ | List item |
| | MaxContextCount | ✅ | ❌ | Max context count display |
| | ProviderAvatar | ✅ | | Provider avatar |
| | ProviderAvatar | ✅ | | Provider avatar |
| | CodeViewer | ❌ | ❌ | Code viewer (external deps) |
| | OGCard | ❌ | ❌ | OG card |
| | MarkdownShadowDOMRenderer | ❌ | ❌ | Markdown renderer |
@ -88,22 +86,11 @@ When submitting PRs, please place components in the correct directory based on t
| | Tab/* | ❌ | ❌ | Tab (Redux dependency) |
| | TopView | ❌ | ❌ | Top view (window.api dependency) |
| **icons** | | | | Icon components |
| | CopyIcon | ✅ | ❌ | Copy icon |
| | DeleteIcon | ✅ | ❌ | Delete icon |
| | EditIcon | ✅ | ❌ | Edit icon |
| | FileIcons | ✅ | ❌ | File icons (includes FileSvgIcon, FilePngIcon) |
| | Icon | ✅ | ✅ | Icon factory function and predefined icons (merged CopyIcon, DeleteIcon, EditIcon, RefreshIcon, ResetIcon, ToolIcon, VisionIcon, WebSearchIcon, WrapIcon, UnWrapIcon, OcrIcon) |
| | FileIcons | ✅ | ❌ | File icons (FileSvgIcon, FilePngIcon) |
| | ReasoningIcon | ✅ | ❌ | Reasoning icon |
| | RefreshIcon | ✅ | ❌ | Refresh icon |
| | ResetIcon | ✅ | ❌ | Reset icon |
| | SvgSpinners180Ring | ✅ | ❌ | Spinners icon |
| | SvgSpinners180Ring | ✅ | ❌ | Spinner loading icon |
| | ToolsCallingIcon | ✅ | ❌ | Tools calling icon |
| | VisionIcon | ✅ | ❌ | Vision icon |
| | WebSearchIcon | ✅ | ❌ | Web search icon |
| | WrapIcon | ✅ | ❌ | Wrap icon |
| | UnWrapIcon | ✅ | ❌ | Unwrap icon |
| | OcrIcon | ✅ | ❌ | OCR icon |
| | ToolIcon | ✅ | ❌ | Tool icon |
| | Other icons | ❌ | ❌ | Other icon files |
| **interactive** | | | | Interactive components |
| | InfoTooltip | ✅ | ❌ | Info tooltip |
| | HelpTooltip | ✅ | ❌ | Help tooltip |

View File

@ -1,69 +1,31 @@
// Original path: src/renderer/src/components/CopyButton.tsx
import { Tooltip } from 'antd'
import { Tooltip } from '@heroui/react'
import { Copy } from 'lucide-react'
import { FC } from 'react'
import styled from 'styled-components'
interface CopyButtonProps {
tooltip?: string
label?: string
color?: string
hoverColor?: string
size?: number
className?: string
[key: string]: any
}
interface ButtonContainerProps {
$color: string
$hoverColor: string
}
const CopyButton: FC<CopyButtonProps> = ({
tooltip,
label,
color = 'var(--color-text-2)',
hoverColor = 'var(--color-primary)',
size = 14,
...props
}) => {
const CopyButton: FC<CopyButtonProps> = ({ tooltip, label, size = 14, className = '', ...props }) => {
const button = (
<ButtonContainer $color={color} $hoverColor={hoverColor} {...props}>
<Copy size={size} className="copy-icon" />
{label && <RightText size={size}>{label}</RightText>}
</ButtonContainer>
<div
className={`flex flex-row items-center gap-1 cursor-pointer text-gray-600 dark:text-gray-400 transition-colors duration-200 hover:text-blue-600 dark:hover:text-blue-400 ${className}`}
{...props}>
<Copy size={size} className="transition-colors duration-200" />
{label && <span style={{ fontSize: `${size}px` }}>{label}</span>}
</div>
)
if (tooltip) {
return <Tooltip title={tooltip}>{button}</Tooltip>
return <Tooltip content={tooltip}>{button}</Tooltip>
}
return button
}
const ButtonContainer = styled.div<ButtonContainerProps>`
display: flex;
flex-direction: row;
align-items: center;
gap: 4px;
cursor: pointer;
color: ${(props) => props.$color};
transition: color 0.2s;
.copy-icon {
color: ${(props) => props.$color};
transition: color 0.2s;
}
&:hover {
color: ${(props) => props.$hoverColor};
.copy-icon {
color: ${(props) => props.$hoverColor};
}
}
`
const RightText = styled.span<{ size: number }>`
font-size: ${(props) => props.size}px;
`
export default CopyButton

View File

@ -1,23 +1,17 @@
// Original path: src/renderer/src/components/CustomCollapse.tsx
import { Collapse } from 'antd'
import { merge } from 'lodash'
import { ChevronRight } from 'lucide-react'
import { FC, memo, useMemo, useState } from 'react'
import { FC, memo, useState } from 'react'
interface CustomCollapseProps {
label: React.ReactNode
extra: React.ReactNode
extra?: React.ReactNode
children: React.ReactNode
destroyInactivePanel?: boolean
defaultActiveKey?: string[]
activeKey?: string[]
collapsible?: 'header' | 'icon' | 'disabled'
onChange?: (activeKeys: string | string[]) => void
style?: React.CSSProperties
styles?: {
header?: React.CSSProperties
body?: React.CSSProperties
}
className?: string
}
const CustomCollapse: FC<CustomCollapseProps> = ({
@ -29,80 +23,47 @@ const CustomCollapse: FC<CustomCollapseProps> = ({
activeKey,
collapsible = undefined,
onChange,
style,
styles
className = ''
}) => {
const [activeKeys, setActiveKeys] = useState(activeKey || defaultActiveKey)
const [isOpen, setIsOpen] = useState(activeKey ? activeKey.includes('1') : defaultActiveKey.includes('1'))
const defaultCollapseStyle = {
width: '100%',
background: 'transparent',
border: '0.5px solid var(--color-border)'
const handleToggle = () => {
if (collapsible === 'disabled') return
const newState = !isOpen
setIsOpen(newState)
onChange?.(newState ? ['1'] : [])
}
const defaultCollpaseHeaderStyle = {
padding: '3px 16px',
alignItems: 'center',
justifyContent: 'space-between',
background: 'var(--color-background-soft)'
}
const getHeaderStyle = () => {
return activeKeys && activeKeys.length > 0
? {
...defaultCollpaseHeaderStyle,
borderTopLeftRadius: '8px',
borderTopRightRadius: '8px'
}
: {
...defaultCollpaseHeaderStyle,
borderRadius: '8px'
}
}
const defaultCollapseItemStyles = {
header: getHeaderStyle(),
body: {
borderTop: 'none'
}
}
const collapseStyle = merge({}, defaultCollapseStyle, style)
const collapseItemStyles = useMemo(() => {
return merge({}, defaultCollapseItemStyles, styles)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeKeys])
const shouldRenderContent = !destroyInactivePanel || isOpen
return (
<Collapse
bordered={false}
style={collapseStyle}
defaultActiveKey={defaultActiveKey}
activeKey={activeKey}
destroyOnHidden={destroyInactivePanel}
collapsible={collapsible}
onChange={(keys) => {
setActiveKeys(keys)
onChange?.(keys)
}}
expandIcon={({ isActive }) => (
<ChevronRight
size={16}
color="var(--color-text-3)"
strokeWidth={1.5}
style={{ transform: isActive ? 'rotate(90deg)' : 'rotate(0deg)' }}
/>
)}
items={[
{
styles: collapseItemStyles,
key: '1',
label,
extra,
children
}
]}
/>
<div className={`w-full bg-transparent border border-gray-200 dark:border-gray-700 ${className}`}>
<div
className={`flex items-center justify-between px-4 py-1 cursor-pointer bg-gray-50 dark:bg-gray-800 ${
isOpen ? 'rounded-t-lg' : 'rounded-lg'
} ${collapsible === 'disabled' ? 'cursor-default' : ''}`}
onClick={collapsible === 'header' || collapsible === undefined ? handleToggle : undefined}>
<div className="flex items-center justify-between w-full">
<div className="flex items-center">
{(collapsible === 'icon' || collapsible === undefined) && (
<div className="mr-2 cursor-pointer" onClick={collapsible === 'icon' ? handleToggle : undefined}>
<ChevronRight
size={16}
className={`text-gray-500 dark:text-gray-400 transition-transform duration-200 ${
isOpen ? 'rotate-90' : 'rotate-0'
}`}
strokeWidth={1.5}
/>
</div>
)}
{label}
</div>
{extra && <div>{extra}</div>}
</div>
</div>
{isOpen && <div className="border-t-0">{shouldRenderContent && children}</div>}
</div>
)
}

View File

@ -1,8 +1,7 @@
// Original path: src/renderer/src/components/Tags/CustomTag.tsx
import { CloseOutlined } from '@ant-design/icons'
import { Tooltip } from 'antd'
import { Tooltip } from '@heroui/react'
import { X } from 'lucide-react'
import { CSSProperties, FC, memo, MouseEventHandler, useMemo } from 'react'
import styled from 'styled-components'
export interface CustomTagProps {
icon?: React.ReactNode
@ -16,6 +15,7 @@ export interface CustomTagProps {
onClick?: MouseEventHandler<HTMLDivElement>
disabled?: boolean
inactive?: boolean
className?: string
}
const CustomTag: FC<CustomTagProps> = ({
@ -29,39 +29,53 @@ const CustomTag: FC<CustomTagProps> = ({
onClose,
onClick,
disabled,
inactive
inactive,
className = ''
}) => {
const actualColor = inactive ? '#aaaaaa' : color
const tagContent = useMemo(
() => (
<Tag
$color={actualColor}
$size={size}
$closable={closable}
$clickable={!disabled && !!onClick}
onClick={disabled ? undefined : onClick}
<div
className={`inline-flex items-center gap-1 rounded-full whitespace-nowrap relative transition-opacity duration-200 ${
!disabled && onClick ? 'cursor-pointer hover:opacity-80' : disabled ? 'cursor-not-allowed' : 'cursor-auto'
} ${className}`}
style={{
...(disabled && { cursor: 'not-allowed' }),
padding: `${size / 3}px ${closable ? size * 1.8 : size * 0.8}px ${size / 3}px ${size * 0.8}px`,
color: actualColor,
backgroundColor: actualColor + '20',
fontSize: `${size}px`,
lineHeight: 1,
...style
}}>
{icon} {children}
}}
onClick={disabled ? undefined : onClick}>
{icon && <span style={{ fontSize: `${size}px`, color: actualColor }}>{icon}</span>}
{children}
{closable && (
<CloseIcon
$size={size}
$color={actualColor}
<div
className="absolute flex items-center justify-center cursor-pointer rounded-full transition-all duration-200 hover:bg-[#da8a8a] hover:text-white"
style={{
right: `${size * 0.2}px`,
top: `${size * 0.2}px`,
bottom: `${size * 0.2}px`,
fontSize: `${size * 0.8}px`,
color: actualColor,
aspectRatio: 1
}}
onClick={(e) => {
e.stopPropagation()
onClose?.()
}}
/>
}}>
<X size={size * 0.8} />
</div>
)}
</Tag>
</div>
),
[actualColor, children, closable, disabled, icon, onClick, onClose, size, style]
[actualColor, children, closable, disabled, icon, onClick, onClose, size, style, className]
)
return tooltip ? (
<Tooltip title={tooltip} placement="top" mouseEnterDelay={0.3}>
<Tooltip content={tooltip} placement="top" delay={300}>
{tagContent}
</Tooltip>
) : (
@ -70,49 +84,3 @@ const CustomTag: FC<CustomTagProps> = ({
}
export default memo(CustomTag)
const Tag = styled.div<{ $color: string; $size: number; $closable: boolean; $clickable: boolean }>`
display: inline-flex;
align-items: center;
gap: 4px;
padding: ${({ $size }) => $size / 3}px ${({ $size }) => $size * 0.8}px;
padding-right: ${({ $closable, $size }) => ($closable ? $size * 1.8 : $size * 0.8)}px;
border-radius: 99px;
color: ${({ $color }) => $color};
background-color: ${({ $color }) => $color + '20'};
font-size: ${({ $size }) => $size}px;
line-height: 1;
white-space: nowrap;
position: relative;
cursor: ${({ $clickable }) => ($clickable ? 'pointer' : 'auto')};
.iconfont {
font-size: ${({ $size }) => $size}px;
color: ${({ $color }) => $color};
}
transition: opacity 0.2s ease;
&:hover {
opacity: ${({ $clickable }) => ($clickable ? 0.8 : 1)};
}
`
const CloseIcon = styled(CloseOutlined)<{ $size: number; $color: string }>`
cursor: pointer;
font-size: ${({ $size }) => $size * 0.8}px;
color: ${({ $color }) => $color};
display: flex;
align-items: center;
justify-content: center;
position: absolute;
right: ${({ $size }) => $size * 0.2}px;
top: ${({ $size }) => $size * 0.2}px;
bottom: ${({ $size }) => $size * 0.2}px;
border-radius: 99px;
transition: all 0.2s ease;
aspect-ratio: 1;
line-height: 1;
&:hover {
background-color: #da8a8a;
color: #ffffff;
}
`

View File

@ -1,37 +1,19 @@
// Original: src/renderer/src/components/DividerWithText.tsx
import React, { CSSProperties } from 'react'
import styled from 'styled-components'
interface DividerWithTextProps {
text: string
style?: CSSProperties
className?: string
}
const DividerWithText: React.FC<DividerWithTextProps> = ({ text, style }) => {
const DividerWithText: React.FC<DividerWithTextProps> = ({ text, style, className = '' }) => {
return (
<DividerContainer style={style}>
<DividerText>{text}</DividerText>
<DividerLine />
</DividerContainer>
<div className={`flex items-center my-0 ${className}`} style={style}>
<span className="text-xs text-gray-600 dark:text-gray-400 mr-2">{text}</span>
<div className="flex-1 h-px bg-gray-200 dark:bg-gray-700" />
</div>
)
}
const DividerContainer = styled.div`
display: flex;
align-items: center;
margin: 0px 0;
`
const DividerText = styled.span`
font-size: 12px;
color: var(--color-text-2);
margin-right: 8px;
`
const DividerLine = styled.div`
flex: 1;
height: 1px;
background-color: var(--color-border);
`
export default DividerWithText

View File

@ -1,6 +1,5 @@
// Original path: src/renderer/src/components/EmojiIcon.tsx
import { FC } from 'react'
import styled from 'styled-components'
interface EmojiIconProps {
emoji: string
@ -9,41 +8,29 @@ interface EmojiIconProps {
fontSize?: number
}
const EmojiIcon: FC<EmojiIconProps> = ({ emoji, className, size = 26, fontSize = 15 }) => {
const EmojiIcon: FC<EmojiIconProps> = ({ emoji, className = '', size = 26, fontSize = 15 }) => {
return (
<Container className={className} $size={size} $fontSize={fontSize}>
<EmojiBackground>{emoji || '⭐️'}</EmojiBackground>
<div
className={`flex items-center justify-center flex-shrink-0 relative overflow-hidden mr-1 rounded-full ${className}`}
style={{
width: `${size}px`,
height: `${size}px`,
borderRadius: `${size / 2}px`,
fontSize: `${fontSize}px`
}}
>
<div
className="absolute inset-0 flex items-center justify-center blur-sm opacity-40"
style={{
fontSize: '200%',
transform: 'scale(1.5)'
}}
>
{emoji || '⭐️'}
</div>
{emoji}
</Container>
</div>
)
}
const Container = styled.div<{ $size: number; $fontSize: number }>`
width: ${({ $size }) => $size}px;
height: ${({ $size }) => $size}px;
border-radius: ${({ $size }) => $size / 2}px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
font-size: ${({ $fontSize }) => $fontSize}px;
position: relative;
overflow: hidden;
margin-right: 3px;
`
const EmojiBackground = styled.div`
width: 100%;
height: 100%;
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 200%;
transform: scale(1.5);
filter: blur(5px);
opacity: 0.4;
`
export default EmojiIcon

View File

@ -1,9 +1,8 @@
// Original path: src/renderer/src/components/ErrorBoundary.tsx
import { Button } from '@heroui/button'
import { Alert, Space } from 'antd'
import { Button } from '@heroui/react'
import { AlertTriangle } from 'lucide-react'
import { ComponentType, ReactNode } from 'react'
import { ErrorBoundary, FallbackProps } from 'react-error-boundary'
import styled from 'styled-components'
import { formatErrorMessage } from './utils'
@ -26,28 +25,29 @@ const DefaultFallback: ComponentType<CustomFallbackProps> = (props: CustomFallba
} = props
return (
<ErrorContainer>
<Alert
message={errorMessage}
showIcon
description={formatErrorMessage(error)}
type="error"
action={
<Space>
{onDebugClick && (
<Button size="sm" onPress={onDebugClick}>
{debugButtonText}
</Button>
)}
{onReloadClick && (
<Button size="sm" onPress={onReloadClick}>
{reloadButtonText}
</Button>
)}
</Space>
}
/>
</ErrorContainer>
<div className="flex justify-center items-center w-full p-2">
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 w-full">
<div className="flex items-start gap-3">
<AlertTriangle className="text-red-500 dark:text-red-400 flex-shrink-0 mt-0.5" size={20} />
<div className="flex-1">
<h3 className="text-red-800 dark:text-red-200 font-medium text-sm mb-1">{errorMessage}</h3>
<p className="text-red-700 dark:text-red-300 text-sm mb-3">{formatErrorMessage(error)}</p>
<div className="flex gap-2">
{onDebugClick && (
<Button size="sm" variant="flat" color="danger" onPress={onDebugClick}>
{debugButtonText}
</Button>
)}
{onReloadClick && (
<Button size="sm" variant="flat" color="danger" onPress={onReloadClick}>
{reloadButtonText}
</Button>
)}
</div>
</div>
</div>
</div>
</div>
)
}
@ -89,13 +89,5 @@ const ErrorBoundaryCustomized = ({
)
}
const ErrorContainer = styled.div`
display: flex;
justify-content: center;
align-items: center;
width: 100%;
padding: 8px;
`
export { ErrorBoundaryCustomized as ErrorBoundary }
export type { CustomFallbackProps, ErrorBoundaryCustomizedProps }

View File

@ -1,6 +1,5 @@
// Original: src/renderer/src/components/IndicatorLight.tsx
import React from 'react'
import styled from 'styled-components'
interface IndicatorLightProps {
color: string
@ -8,38 +7,31 @@ interface IndicatorLightProps {
shadow?: boolean
style?: React.CSSProperties
animation?: boolean
className?: string
}
const Light = styled.div<{
color: string
size: number
shadow?: boolean
style?: React.CSSProperties
animation?: boolean
}>`
width: ${({ size }) => size}px;
height: ${({ size }) => size}px;
border-radius: 50%;
background-color: ${({ color }) => color};
box-shadow: ${({ shadow, color }) => (shadow ? `0 0 6px ${color}` : 'none')};
animation: ${({ animation }) => (animation ? 'pulse 2s infinite' : 'none')};
@keyframes pulse {
0% {
opacity: 1;
}
50% {
opacity: 0.6;
}
100% {
opacity: 1;
}
}
`
const IndicatorLight: React.FC<IndicatorLightProps> = ({ color, size = 8, shadow = true, style, animation = true }) => {
const IndicatorLight: React.FC<IndicatorLightProps> = ({
color,
size = 8,
shadow = true,
style,
animation = true,
className = ''
}) => {
const actualColor = color === 'green' ? '#22c55e' : color
return <Light color={actualColor} size={size} shadow={shadow} style={style} animation={animation} />
return (
<div
className={`rounded-full ${animation ? 'animate-pulse' : ''} ${className}`}
style={{
width: `${size}px`,
height: `${size}px`,
backgroundColor: actualColor,
boxShadow: shadow ? `0 0 6px ${actualColor}` : 'none',
...style
}}
/>
)
}
export default IndicatorLight

View File

@ -1,10 +1,10 @@
// Original: src/renderer/src/components/Spinner.tsx
import { motion } from 'framer-motion'
import { Search } from 'lucide-react'
import styled from 'styled-components'
interface Props {
text: React.ReactNode
className?: string
}
// Define variants for the spinner animation
@ -17,9 +17,10 @@ const spinnerVariants = {
}
}
export default function Spinner({ text }: Props) {
export default function Spinner({ text, className = '' }: Props) {
return (
<Searching
<motion.div
className={`flex items-center gap-1 p-0 ${className}`}
variants={spinnerVariants}
initial="defaultColor"
animate={['defaultColor', 'dimmed']}
@ -31,15 +32,6 @@ export default function Spinner({ text }: Props) {
}}>
<Search size={16} style={{ color: 'unset' }} />
<span>{text}</span>
</Searching>
</motion.div>
)
}
const SearchWrapper = styled.div`
display: flex;
align-items: center;
gap: 4px;
/* font-size: 14px; */
padding: 0px;
/* padding-left: 0; */
`
const Searching = motion.create(SearchWrapper)

View File

@ -11,6 +11,7 @@ export interface StatusTagProps {
iconSize?: number
icon?: React.ReactNode
color?: string
className?: string
}
const statusConfig: Record<StatusType, { Icon: LucideIcon; color: string }> = {
@ -20,14 +21,14 @@ const statusConfig: Record<StatusType, { Icon: LucideIcon; color: string }> = {
info: { Icon: InfoIcon, color: '#3B82F6' } // blue-500
}
export const StatusTag: React.FC<StatusTagProps> = ({ type, message, iconSize = 14, icon, color }) => {
export const StatusTag: React.FC<StatusTagProps> = ({ type, message, iconSize = 14, icon, color, className }) => {
const config = statusConfig[type]
const Icon = config.Icon
const finalColor = color || config.color
const finalIcon = icon || <Icon size={iconSize} color={finalColor} />
return (
<CustomTag icon={finalIcon} color={finalColor}>
<CustomTag icon={finalIcon} color={finalColor} className={className}>
{message}
</CustomTag>
)

View File

@ -1,23 +1,20 @@
// Original: src/renderer/src/components/TextBadge.tsx
import { FC } from 'react'
import styled from 'styled-components'
interface Props {
interface TextBadgeProps {
text: string
style?: React.CSSProperties
className?: string
}
const TextBadge: FC<Props> = ({ text, style }) => {
return <Container style={style}>{text}</Container>
const TextBadge: FC<TextBadgeProps> = ({ text, style, className = '' }) => {
return (
<span
className={`text-xs text-blue-600 dark:text-blue-400 bg-blue-100 dark:bg-blue-900/30 px-1.5 py-0.5 rounded font-medium ${className}`}
style={style}>
{text}
</span>
)
}
const Container = styled.span`
font-size: 12px;
color: var(--color-primary);
background: var(--color-primary-bg);
padding: 2px 6px;
border-radius: 4px;
font-weight: 500;
`
export default TextBadge

View File

@ -1,6 +1,5 @@
// Original path: src/renderer/src/components/Avatar/EmojiAvatar.tsx
import React, { memo } from 'react'
import styled from 'styled-components'
import { memo } from 'react'
interface EmojiAvatarProps {
children: string
@ -9,46 +8,28 @@ interface EmojiAvatarProps {
onClick?: React.MouseEventHandler<HTMLDivElement>
className?: string
style?: React.CSSProperties
ref?: React.RefObject<HTMLDivElement>
}
const EmojiAvatar = ({
ref,
children,
size = 31,
fontSize,
onClick,
className,
style
}: EmojiAvatarProps & { ref?: React.RefObject<HTMLDivElement | null> }) => (
<StyledEmojiAvatar
ref={ref}
$size={size}
$fontSize={fontSize ?? size * 0.5}
onClick={onClick}
className={className}
style={style}>
{children}
</StyledEmojiAvatar>
)
const EmojiAvatar = ({ children, size = 31, fontSize, onClick, className = '', style, ref }: EmojiAvatarProps) => {
const computedFontSize = fontSize ?? size * 0.5
return (
<div
ref={ref}
onClick={onClick}
className={`flex items-center justify-center rounded-[20%] border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 cursor-pointer transition-opacity hover:opacity-80 ${className}`}
style={{
width: `${size}px`,
height: `${size}px`,
fontSize: `${computedFontSize}px`,
...style
}}>
{children}
</div>
)
}
EmojiAvatar.displayName = 'EmojiAvatar'
const StyledEmojiAvatar = styled.div<{ $size: number; $fontSize: number }>`
display: flex;
align-items: center;
justify-content: center;
background-color: var(--color-background-soft);
border: 0.5px solid var(--color-border);
border-radius: 20%;
cursor: pointer;
width: ${(props) => props.$size}px;
height: ${(props) => props.$size}px;
font-size: ${(props) => props.$fontSize}px;
transition: opacity 0.3s ease;
&:hover {
opacity: 0.8;
}
`
export default memo(EmojiAvatar)

View File

@ -1,52 +1,59 @@
// Original: src/renderer/src/components/ExpandableText.tsx
import { Button } from 'antd'
import { memo, useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { Button } from '@heroui/react'
import { memo, useCallback, useState } from 'react'
interface ExpandableTextProps {
text: string
style?: React.CSSProperties
className?: string
expandText?: string
collapseText?: string
lineClamp?: number
ref?: React.RefObject<HTMLDivElement>
}
const ExpandableText = ({
ref,
text,
style
}: ExpandableTextProps & { ref?: React.RefObject<HTMLParagraphElement> | null }) => {
const { t } = useTranslation()
const [isExpanded, setIsExpanded] = useState(false)
style,
className = '',
expandText = 'Expand',
collapseText = 'Collapse',
lineClamp = 1,
ref
}: ExpandableTextProps) => {
const [isExpanded, setIsExpanded] = useState(false)
const toggleExpand = useCallback(() => {
setIsExpanded((prev) => !prev)
}, [])
const toggleExpand = useCallback(() => {
setIsExpanded((prev) => !prev)
}, [])
const button = useMemo(() => {
return (
<Button type="link" onClick={toggleExpand} style={{ alignSelf: 'flex-end' }}>
{isExpanded ? t('common.collapse') : t('common.expand')}
</Button>
<div
ref={ref}
className={`flex ${isExpanded ? 'flex-col' : 'flex-row items-center'} gap-2 ${className}`}
style={style}>
<div
className={`overflow-hidden ${
isExpanded
? ''
: lineClamp === 1
? 'text-ellipsis whitespace-nowrap'
: `line-clamp-${lineClamp}`
} ${isExpanded ? '' : 'flex-1'}`}>
{text}
</div>
<Button
size="sm"
variant="light"
color="primary"
onClick={toggleExpand}
className="min-w-fit px-2">
{isExpanded ? collapseText : expandText}
</Button>
</div>
)
}, [isExpanded, t, toggleExpand])
}
return (
<Container ref={ref} style={style} $expanded={isExpanded}>
<TextContainer $expanded={isExpanded}>{text}</TextContainer>
{button}
</Container>
)
}
const Container = styled.div<{ $expanded?: boolean }>`
display: flex;
flex-direction: ${(props) => (props.$expanded ? 'column' : 'row')};
`
const TextContainer = styled.div<{ $expanded?: boolean }>`
overflow: hidden;
text-overflow: ${(props) => (props.$expanded ? 'unset' : 'ellipsis')};
white-space: ${(props) => (props.$expanded ? 'normal' : 'nowrap')};
line-height: ${(props) => (props.$expanded ? 'unset' : '30px')};
`
ExpandableText.displayName = 'ExpandableText'
export default memo(ExpandableText)

View File

@ -1,7 +1,6 @@
// Original path: src/renderer/src/components/ProviderAvatar.tsx
import { Avatar } from 'antd'
import { Avatar } from '@heroui/react'
import React from 'react'
import styled from 'styled-components'
import { generateColorFromChar, getFirstCharacter, getForegroundColor } from './utils'
@ -9,50 +8,55 @@ interface ProviderAvatarProps {
providerId: string
providerName: string
logoSrc?: string
size?: number
size?: 'sm' | 'md' | 'lg' | number
className?: string
style?: React.CSSProperties
renderCustomLogo?: (providerId: string) => React.ReactNode
}
const ProviderLogo = styled(Avatar)`
width: 100%;
height: 100%;
border: 0.5px solid var(--color-border);
`
const ProviderSvgLogo = styled.div`
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
border: 0.5px solid var(--color-border);
border-radius: 100%;
& > svg {
width: 80%;
height: 80%;
}
`
export const ProviderAvatar: React.FC<ProviderAvatarProps> = ({
providerId,
providerName,
logoSrc,
size,
size = 'md',
className,
style,
renderCustomLogo
}) => {
// Convert numeric size to HeroUI size props
const getAvatarSize = () => {
if (typeof size === 'number') {
// For custom numeric sizes, we'll use style override
return 'md'
}
return size
}
const getCustomStyle = () => {
const baseStyle: React.CSSProperties = {
...style
}
if (typeof size === 'number') {
baseStyle.width = `${size}px`
baseStyle.height = `${size}px`
}
return baseStyle
}
// Check if custom logo renderer is provided for special providers
if (renderCustomLogo) {
const customLogo = renderCustomLogo(providerId)
if (customLogo) {
return (
<ProviderSvgLogo className={className} style={style}>
{customLogo}
</ProviderSvgLogo>
<div
className={`flex items-center justify-center rounded-full border border-gray-200 dark:border-gray-700 ${className || ''}`}
style={getCustomStyle()}>
<div className="w-4/5 h-4/5 flex items-center justify-center">
{customLogo}
</div>
</div>
)
}
}
@ -60,7 +64,13 @@ export const ProviderAvatar: React.FC<ProviderAvatarProps> = ({
// If logo source is provided, render image avatar
if (logoSrc) {
return (
<ProviderLogo draggable="false" shape="circle" src={logoSrc} className={className} style={style} size={size} />
<Avatar
src={logoSrc}
size={getAvatarSize()}
className={`border border-gray-200 dark:border-gray-700 ${className || ''}`}
style={getCustomStyle()}
imgProps={{ draggable: false }}
/>
)
}
@ -69,17 +79,16 @@ export const ProviderAvatar: React.FC<ProviderAvatarProps> = ({
const color = providerName ? getForegroundColor(backgroundColor) : 'white'
return (
<ProviderLogo
size={size}
shape="circle"
className={className}
<Avatar
name={getFirstCharacter(providerName)}
size={getAvatarSize()}
className={`border border-gray-200 dark:border-gray-700 ${className || ''}`}
style={{
backgroundColor,
color,
...style
}}>
{getFirstCharacter(providerName)}
</ProviderLogo>
...getCustomStyle()
}}
/>
)
}

View File

@ -7,14 +7,14 @@ import styled from 'styled-components'
import { lightbulbVariants } from './defaultVariants'
interface Props {
interface ThinkingEffectProps {
isThinking: boolean
thinkingTimeText: React.ReactNode
content: string
expanded: boolean
}
const ThinkingEffect: React.FC<Props> = ({ isThinking, thinkingTimeText, content, expanded }) => {
const ThinkingEffect: React.FC<ThinkingEffectProps> = ({ isThinking, thinkingTimeText, content, expanded }) => {
const [messages, setMessages] = useState<string[]>([])
useEffect(() => {

View File

@ -0,0 +1,123 @@
import type { Meta, StoryObj } from '@storybook/react'
import CopyButton from '../../../src/components/base/CopyButton'
const meta: Meta<typeof CopyButton> = {
title: 'Base/CopyButton',
component: CopyButton,
parameters: {
layout: 'centered'
},
tags: ['autodocs'],
argTypes: {
tooltip: {
control: 'text',
description: '悬停时显示的提示文字'
},
label: {
control: 'text',
description: '复制按钮的标签文字'
},
size: {
control: { type: 'range', min: 10, max: 30, step: 1 },
description: '图标和文字的大小'
},
className: {
control: 'text',
description: '自定义 CSS 类名'
}
}
}
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {}
}
export const WithTooltip: Story = {
args: {
tooltip: '点击复制'
}
}
export const WithLabel: Story = {
args: {
label: '复制'
}
}
export const WithTooltipAndLabel: Story = {
args: {
tooltip: '点击复制内容到剪贴板',
label: '复制内容'
}
}
export const SmallSize: Story = {
args: {
size: 12,
label: '小尺寸',
tooltip: '小尺寸复制按钮'
}
}
export const LargeSize: Story = {
args: {
size: 20,
label: '大尺寸',
tooltip: '大尺寸复制按钮'
}
}
export const CustomStyle: Story = {
args: {
label: '自定义样式',
tooltip: '自定义样式的复制按钮',
className: 'bg-blue-50 dark:bg-blue-900/20 p-2 rounded-lg border-2 border-blue-200 dark:border-blue-700'
}
}
export const OnlyIcon: Story = {
args: {
tooltip: '仅图标模式',
size: 16
}
}
export const Interactive: Story = {
args: {
tooltip: '可交互的复制按钮',
label: '点击复制'
},
render: (args) => (
<div className="space-y-4">
<div>
<h3 className="text-sm font-medium mb-2">:</h3>
<div className="space-y-2">
<div>
<CopyButton {...args} onClick={() => alert('已复制!')} />
</div>
<div>
<CopyButton tooltip="禁用状态" label="禁用" className="opacity-50 pointer-events-none" />
</div>
</div>
</div>
</div>
)
}
export const MultipleButtons: Story = {
render: () => (
<div className="space-y-4">
<h3 className="text-sm font-medium mb-2">:</h3>
<div className="flex flex-wrap gap-4">
<CopyButton tooltip="复制代码" label="代码" size={14} />
<CopyButton tooltip="复制链接" label="链接" size={14} />
<CopyButton tooltip="复制文本" label="文本" size={14} />
<CopyButton tooltip="复制JSON" label="JSON" size={14} />
</div>
</div>
)
}

View File

@ -0,0 +1,284 @@
import { Button } from '@heroui/react'
import type { Meta, StoryObj } from '@storybook/react'
import { AlertTriangle, Info, Settings } from 'lucide-react'
import { useState } from 'react'
import CustomCollapse from '../../../src/components/base/CustomCollapse'
const meta: Meta<typeof CustomCollapse> = {
title: 'Base/CustomCollapse',
component: CustomCollapse,
parameters: {
layout: 'padded'
},
tags: ['autodocs'],
argTypes: {
label: {
control: false,
description: '折叠面板的标题内容'
},
extra: {
control: false,
description: '标题栏右侧的额外内容'
},
children: {
control: false,
description: '折叠面板的内容'
},
destroyInactivePanel: {
control: 'boolean',
description: '是否销毁非活动面板的内容'
},
defaultActiveKey: {
control: false,
description: '默认激活的面板键值'
},
activeKey: {
control: false,
description: '当前激活的面板键值(受控模式)'
},
collapsible: {
control: 'select',
options: ['header', 'icon', 'disabled', undefined],
description: '折叠触发方式'
},
onChange: {
control: false,
description: '面板状态变化回调'
}
}
}
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
label: '默认折叠面板',
children: (
<div className="p-4">
<p></p>
<p></p>
</div>
)
}
}
export const WithExtra: Story = {
args: {
label: '带额外内容的面板',
extra: (
<Button size="sm" variant="ghost">
</Button>
),
children: (
<div className="p-4">
<p></p>
<p>/</p>
</div>
)
}
}
export const WithIcon: Story = {
args: {
label: (
<div className="flex items-center gap-2">
<Settings size={16} />
<span></span>
</div>
),
children: (
<div className="p-4">
<div className="space-y-3">
<div className="flex items-center justify-between">
<span></span>
<Button size="sm" variant="flat">
</Button>
</div>
<div className="flex items-center justify-between">
<span></span>
<Button size="sm" variant="flat">
</Button>
</div>
</div>
</div>
)
}
}
export const CollapsibleHeader: Story = {
args: {
label: '点击整个标题栏展开/收起',
collapsible: 'header',
children: (
<div className="p-4">
<p> collapsible="header"/</p>
</div>
)
}
}
export const CollapsibleIcon: Story = {
args: {
label: '仅点击图标展开/收起',
collapsible: 'icon',
children: (
<div className="p-4">
<p> collapsible="icon"/</p>
</div>
)
}
}
export const Disabled: Story = {
args: {
label: '禁用的折叠面板',
collapsible: 'disabled',
children: (
<div className="p-4">
<p></p>
</div>
)
}
}
export const DestroyInactivePanel: Story = {
args: {
label: '销毁非活动内容',
destroyInactivePanel: true,
children: (
<div className="p-4">
<p> destroyInactivePanel=true </p>
<p>{new Date().toLocaleTimeString()}</p>
</div>
)
}
}
export const RichContent: Story = {
args: {
label: (
<div className="flex items-center gap-2">
<Info size={16} />
<span></span>
</div>
),
extra: (
<div className="flex gap-2">
<Button size="sm" variant="flat" color="primary">
</Button>
<Button size="sm" variant="flat">
</Button>
</div>
),
children: (
<div className="p-4 space-y-4">
<div>
<h4 className="font-medium mb-2"></h4>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1"></label>
<input
type="text"
className="w-full px-3 py-2 border border-gray-300 rounded-md"
placeholder="请输入名称"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1"></label>
<select className="w-full px-3 py-2 border border-gray-300 rounded-md">
<option></option>
<option> A</option>
<option> B</option>
</select>
</div>
</div>
</div>
<div>
<label className="block text-sm font-medium mb-1"></label>
<textarea className="w-full px-3 py-2 border border-gray-300 rounded-md" rows={3} placeholder="请输入描述" />
</div>
</div>
)
}
}
export const MultipleCollapse: Story = {
render: () => (
<div className="space-y-4">
<h3 className="text-lg font-medium"></h3>
<div className="space-y-2">
<CustomCollapse
label="面板 1"
defaultActiveKey={['1']}
children={
<div className="p-4">
<p></p>
</div>
}
/>
<CustomCollapse
label={
<div className="flex items-center gap-2">
<AlertTriangle size={16} className="text-yellow-500" />
<span></span>
</div>
}
defaultActiveKey={[]}
children={
<div className="p-4 bg-yellow-50 dark:bg-yellow-900/20">
<p className="text-yellow-800 dark:text-yellow-200"></p>
</div>
}
/>
<CustomCollapse
label="面板 3"
collapsible="icon"
extra={<span className="text-sm text-gray-500"></span>}
defaultActiveKey={[]}
children={
<div className="p-4">
<p>/</p>
</div>
}
/>
</div>
</div>
)
}
export const ControlledMode: Story = {
render: function ControlledMode() {
const [activeKey, setActiveKey] = useState<string[]>(['1'])
return (
<div className="space-y-4">
<div className="flex gap-2">
<Button size="sm" onPress={() => setActiveKey(['1'])} color={activeKey.includes('1') ? 'primary' : 'default'}>
</Button>
<Button size="sm" onPress={() => setActiveKey([])} color={!activeKey.includes('1') ? 'primary' : 'default'}>
</Button>
</div>
<CustomCollapse
label="受控模式"
activeKey={activeKey}
onChange={(keys) => setActiveKey(Array.isArray(keys) ? keys : [keys])}
children={
<div className="p-4">
<p>/</p>
<p>{activeKey.includes('1') ? '展开' : '收起'}</p>
</div>
}
/>
</div>
)
}
}

View File

@ -0,0 +1,214 @@
import type { Meta, StoryObj } from '@storybook/react'
import DividerWithText from '../../../src/components/base/DividerWithText'
const meta: Meta<typeof DividerWithText> = {
title: 'Base/DividerWithText',
component: DividerWithText,
parameters: {
layout: 'padded'
},
tags: ['autodocs'],
argTypes: {
text: {
control: 'text',
description: '分割线上显示的文字'
},
style: {
control: false,
description: '自定义样式对象'
}
}
}
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
text: '分割线'
}
}
export const ShortText: Story = {
args: {
text: '或'
}
}
export const LongText: Story = {
args: {
text: '这是一个较长的分割线文字'
}
}
export const EnglishText: Story = {
args: {
text: 'OR'
}
}
export const WithNumbers: Story = {
args: {
text: '步骤 1'
}
}
export const WithSymbols: Story = {
args: {
text: '• • •'
}
}
export const CustomStyle: Story = {
args: {
text: '自定义样式',
style: {
marginTop: '16px',
marginBottom: '16px'
}
}
}
export const MultipleUsage: Story = {
render: () => (
<div className="space-y-8">
<div>
<h3 className="text-lg font-medium mb-4"></h3>
<div className="max-w-md mx-auto space-y-4">
<div>
<label className="block text-sm font-medium mb-1"></label>
<input
type="email"
className="w-full px-3 py-2 border border-gray-300 rounded-md"
placeholder="请输入邮箱"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1"></label>
<input
type="password"
className="w-full px-3 py-2 border border-gray-300 rounded-md"
placeholder="请输入密码"
/>
</div>
<button className="w-full bg-blue-600 text-white py-2 rounded-md hover:bg-blue-700"></button>
<DividerWithText text="或" />
<button className="w-full border border-gray-300 py-2 rounded-md hover:bg-gray-50">使 Google </button>
<button className="w-full border border-gray-300 py-2 rounded-md hover:bg-gray-50">使 GitHub </button>
</div>
</div>
</div>
)
}
export const InSections: Story = {
render: () => (
<div className="space-y-6">
<div>
<h2 className="text-xl font-bold mb-4"></h2>
<p className="text-gray-600 mb-4"></p>
<DividerWithText text="正文开始" />
<p className="text-gray-600 mb-4"></p>
<p className="text-gray-600 mb-4"></p>
<DividerWithText text="总结" />
<p className="text-gray-600"></p>
</div>
</div>
)
}
export const WithSteps: Story = {
render: () => (
<div className="space-y-6">
<h3 className="text-lg font-medium"></h3>
<div className="space-y-1">
<p className="text-sm"></p>
</div>
<DividerWithText text="步骤 1 完成" />
<div className="space-y-1">
<p className="text-sm"></p>
</div>
<DividerWithText text="步骤 2 完成" />
<div className="space-y-1">
<p className="text-sm"></p>
</div>
<DividerWithText text="步骤 3 完成" />
<div className="space-y-1">
<p className="text-sm font-medium text-green-600"></p>
</div>
</div>
)
}
export const DifferentSizes: Story = {
render: () => (
<div className="space-y-6">
<h3 className="text-lg font-medium">线</h3>
<div className="space-y-4">
<DividerWithText text="默认样式" />
<DividerWithText text="加粗文字" className="[&>span]:font-bold" />
<DividerWithText text="彩色文字" className="[&>span]:text-blue-600 [&>span]:dark:text-blue-400" />
<DividerWithText text="较大文字" className="[&>span]:text-sm" />
<DividerWithText
text="带背景的文字"
className="[&>span]:bg-gray-100 [&>span]:dark:bg-gray-800 [&>span]:px-2 [&>span]:py-1 [&>span]:rounded"
/>
</div>
</div>
)
}
export const Timeline: Story = {
render: () => (
<div className="space-y-4">
<h3 className="text-lg font-medium">线</h3>
<div className="space-y-3">
<div className="p-3 bg-blue-50 dark:bg-blue-900/20 rounded">
<h4 className="font-medium"></h4>
<p className="text-sm text-gray-600 dark:text-gray-400"></p>
</div>
<DividerWithText text="2024年1月" />
<div className="p-3 bg-green-50 dark:bg-green-900/20 rounded">
<h4 className="font-medium"></h4>
<p className="text-sm text-gray-600 dark:text-gray-400"></p>
</div>
<DividerWithText text="2024年3月" />
<div className="p-3 bg-yellow-50 dark:bg-yellow-900/20 rounded">
<h4 className="font-medium"></h4>
<p className="text-sm text-gray-600 dark:text-gray-400"></p>
</div>
<DividerWithText text="2024年5月" />
<div className="p-3 bg-purple-50 dark:bg-purple-900/20 rounded">
<h4 className="font-medium">线</h4>
<p className="text-sm text-gray-600 dark:text-gray-400"></p>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,310 @@
import type { Meta, StoryObj } from '@storybook/react'
import EmojiIcon from '../../../src/components/base/EmojiIcon'
const meta: Meta<typeof EmojiIcon> = {
title: 'Base/EmojiIcon',
component: EmojiIcon,
parameters: {
layout: 'centered'
},
tags: ['autodocs'],
argTypes: {
emoji: {
control: 'text',
description: '要显示的 emoji 字符'
},
className: {
control: 'text',
description: '自定义 CSS 类名'
},
size: {
control: { type: 'range', min: 16, max: 80, step: 2 },
description: '图标容器的大小(像素)'
},
fontSize: {
control: { type: 'range', min: 8, max: 40, step: 1 },
description: 'emoji 的字体大小(像素)'
}
}
}
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {}
}
export const Star: Story = {
args: {
emoji: '⭐️'
}
}
export const Heart: Story = {
args: {
emoji: '❤️'
}
}
export const Smile: Story = {
args: {
emoji: '😊'
}
}
export const Fire: Story = {
args: {
emoji: '🔥'
}
}
export const Rocket: Story = {
args: {
emoji: '🚀'
}
}
export const SmallSize: Story = {
args: {
emoji: '🎯',
size: 20,
fontSize: 12
}
}
export const LargeSize: Story = {
args: {
emoji: '🌟',
size: 60,
fontSize: 30
}
}
export const CustomStyle: Story = {
args: {
emoji: '💎',
size: 40,
fontSize: 20,
className: 'border-2 border-blue-300 dark:border-blue-600 shadow-lg'
}
}
export const EmojiCollection: Story = {
render: () => (
<div className="space-y-6">
<div>
<h3 className="text-lg font-medium mb-4"></h3>
<div className="grid grid-cols-6 gap-4">
{[
'😀',
'😃',
'😄',
'😁',
'😊',
'😍',
'🤔',
'😎',
'🤗',
'😴',
'🙄',
'😇',
'❤️',
'💙',
'💚',
'💛',
'🧡',
'💜',
'⭐',
'🌟',
'✨',
'🔥',
'💎',
'🎯',
'🚀',
'⚡',
'🌈',
'🎉',
'🎊',
'🏆'
].map((emoji, index) => (
<EmojiIcon key={index} emoji={emoji} size={32} fontSize={16} />
))}
</div>
</div>
</div>
)
}
export const SizeComparison: Story = {
render: () => (
<div className="space-y-6">
<h3 className="text-lg font-medium mb-4"></h3>
<div className="flex items-center gap-6">
<div className="text-center">
<EmojiIcon emoji="🎨" size={20} fontSize={12} />
<p className="text-xs mt-2"> (20px)</p>
</div>
<div className="text-center">
<EmojiIcon emoji="🎨" size={30} fontSize={16} />
<p className="text-xs mt-2"> (30px)</p>
</div>
<div className="text-center">
<EmojiIcon emoji="🎨" size={40} fontSize={20} />
<p className="text-xs mt-2"> (40px)</p>
</div>
<div className="text-center">
<EmojiIcon emoji="🎨" size={60} fontSize={30} />
<p className="text-xs mt-2"> (60px)</p>
</div>
</div>
</div>
)
}
export const InUserInterface: Story = {
render: () => (
<div className="space-y-6">
<h3 className="text-lg font-medium mb-4"></h3>
{/* 用户头像 */}
<div className="space-y-3">
<h4 className="font-medium"></h4>
<div className="flex items-center gap-3">
<EmojiIcon emoji="👤" size={40} fontSize={20} />
<div>
<p className="font-medium"></p>
<p className="text-sm text-gray-500">user@example.com</p>
</div>
</div>
</div>
{/* 状态指示器 */}
<div className="space-y-3">
<h4 className="font-medium"></h4>
<div className="space-y-2">
<div className="flex items-center gap-2">
<EmojiIcon emoji="✅" size={24} fontSize={14} />
<span></span>
</div>
<div className="flex items-center gap-2">
<EmojiIcon emoji="⏳" size={24} fontSize={14} />
<span></span>
</div>
<div className="flex items-center gap-2">
<EmojiIcon emoji="❌" size={24} fontSize={14} />
<span></span>
</div>
</div>
</div>
{/* 导航菜单 */}
<div className="space-y-3">
<h4 className="font-medium"></h4>
<div className="space-y-1">
<div className="flex items-center gap-3 p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded cursor-pointer">
<EmojiIcon emoji="🏠" size={24} fontSize={14} />
<span></span>
</div>
<div className="flex items-center gap-3 p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded cursor-pointer">
<EmojiIcon emoji="📊" size={24} fontSize={14} />
<span></span>
</div>
<div className="flex items-center gap-3 p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded cursor-pointer">
<EmojiIcon emoji="⚙️" size={24} fontSize={14} />
<span></span>
</div>
</div>
</div>
</div>
)
}
export const CategoryIcons: Story = {
render: () => (
<div className="space-y-6">
<h3 className="text-lg font-medium mb-4"></h3>
<div className="grid grid-cols-2 gap-6">
<div>
<h4 className="font-medium mb-3"></h4>
<div className="space-y-2">
<div className="flex items-center gap-2">
<EmojiIcon emoji="💼" size={24} fontSize={14} />
<span></span>
</div>
<div className="flex items-center gap-2">
<EmojiIcon emoji="📈" size={24} fontSize={14} />
<span></span>
</div>
<div className="flex items-center gap-2">
<EmojiIcon emoji="💻" size={24} fontSize={14} />
<span></span>
</div>
</div>
</div>
<div>
<h4 className="font-medium mb-3"></h4>
<div className="space-y-2">
<div className="flex items-center gap-2">
<EmojiIcon emoji="🍕" size={24} fontSize={14} />
<span></span>
</div>
<div className="flex items-center gap-2">
<EmojiIcon emoji="✈️" size={24} fontSize={14} />
<span></span>
</div>
<div className="flex items-center gap-2">
<EmojiIcon emoji="🎵" size={24} fontSize={14} />
<span></span>
</div>
</div>
</div>
</div>
</div>
)
}
export const AnimatedExample: Story = {
render: () => (
<div className="space-y-6">
<h3 className="text-lg font-medium mb-4"></h3>
<div className="flex gap-4">
{['🎉', '🎊', '✨', '🌟', '⭐'].map((emoji, index) => (
<div
key={index}
className="cursor-pointer transition-transform duration-200 hover:scale-110"
onClick={() => alert(`点击了 ${emoji}`)}>
<EmojiIcon emoji={emoji} size={36} fontSize={18} />
</div>
))}
</div>
<p className="text-sm text-gray-500"></p>
</div>
)
}
export const BlurEffect: Story = {
render: () => (
<div className="space-y-6">
<h3 className="text-lg font-medium mb-4"></h3>
<p className="text-sm text-gray-600 mb-4">EmojiIcon emoji </p>
<div className="flex gap-6">
<div className="text-center">
<EmojiIcon emoji="🌙" size={50} fontSize={25} />
<p className="text-xs mt-2"></p>
</div>
<div className="text-center">
<EmojiIcon emoji="☀️" size={50} fontSize={25} />
<p className="text-xs mt-2"></p>
</div>
<div className="text-center">
<EmojiIcon emoji="🌈" size={50} fontSize={25} />
<p className="text-xs mt-2"></p>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,249 @@
import { Button } from '@heroui/react'
import type { Meta, StoryObj } from '@storybook/react'
import { useState } from 'react'
import { CustomFallbackProps, ErrorBoundary } from '../../../src/components/base/ErrorBoundary'
// 错误组件 - 用于触发错误
const ThrowErrorComponent = ({ shouldThrow = false, errorMessage = '这是一个模拟错误' }) => {
if (shouldThrow) {
throw new Error(errorMessage)
}
return <div className="p-4 bg-green-50 dark:bg-green-900/20 rounded"></div>
}
// 异步错误组件
const AsyncErrorComponent = () => {
const [error, setError] = useState(false)
const handleAsyncError = () => {
setTimeout(() => {
setError(true)
}, 1000)
}
if (error) {
throw new Error('异步操作失败')
}
return (
<div className="p-4 space-y-2">
<p></p>
<Button onPress={handleAsyncError}>1</Button>
</div>
)
}
const meta: Meta<typeof ErrorBoundary> = {
title: 'Base/ErrorBoundary',
component: ErrorBoundary,
parameters: {
layout: 'padded'
},
tags: ['autodocs'],
argTypes: {
children: {
control: false,
description: '被错误边界包裹的子组件'
},
fallbackComponent: {
control: false,
description: '自定义错误回退组件'
},
onDebugClick: {
control: false,
description: '调试按钮点击回调'
},
onReloadClick: {
control: false,
description: '重新加载按钮点击回调'
},
debugButtonText: {
control: 'text',
description: '调试按钮文字'
},
reloadButtonText: {
control: 'text',
description: '重新加载按钮文字'
},
errorMessage: {
control: 'text',
description: '错误消息标题'
}
}
}
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
render: () => (
<ErrorBoundary>
<ThrowErrorComponent shouldThrow={true} />
</ErrorBoundary>
)
}
export const CustomErrorMessage: Story = {
render: () => (
<ErrorBoundary errorMessage="自定义错误消息">
<ThrowErrorComponent shouldThrow={true} errorMessage="这是一个自定义的错误消息" />
</ErrorBoundary>
)
}
export const WithDebugButton: Story = {
render: () => (
<ErrorBoundary onDebugClick={() => alert('打开调试工具')} debugButtonText="打开调试">
<ThrowErrorComponent shouldThrow={true} />
</ErrorBoundary>
)
}
export const WithReloadButton: Story = {
render: () => (
<ErrorBoundary onReloadClick={() => window.location.reload()} reloadButtonText="重新加载页面">
<ThrowErrorComponent shouldThrow={true} />
</ErrorBoundary>
)
}
export const WithBothButtons: Story = {
render: () => (
<ErrorBoundary
onDebugClick={() => alert('打开开发者工具')}
onReloadClick={() => alert('重新加载应用')}
debugButtonText="调试"
reloadButtonText="重载"
errorMessage="应用程序遇到错误">
<ThrowErrorComponent shouldThrow={true} errorMessage="组件渲染失败" />
</ErrorBoundary>
)
}
export const NoError: Story = {
render: () => (
<ErrorBoundary>
<ThrowErrorComponent shouldThrow={false} />
</ErrorBoundary>
)
}
export const InteractiveDemo: Story = {
render: function InteractiveDemo() {
const [shouldThrow, setShouldThrow] = useState(false)
const [errorMessage, setErrorMessage] = useState('用户触发的错误')
return (
<div className="space-y-4">
<div className="flex gap-2">
<Button color={shouldThrow ? 'danger' : 'primary'} onPress={() => setShouldThrow(!shouldThrow)}>
{shouldThrow ? '取消错误' : '触发错误'}
</Button>
<input
type="text"
value={errorMessage}
onChange={(e) => setErrorMessage(e.target.value)}
placeholder="自定义错误消息"
className="px-3 py-1 border border-gray-300 rounded text-sm"
/>
</div>
<ErrorBoundary
key={shouldThrow ? 'error' : 'normal'} // 重置错误边界
onDebugClick={() => console.log('Debug clicked')}
onReloadClick={() => setShouldThrow(false)}
debugButtonText="控制台调试"
reloadButtonText="重置组件"
errorMessage="交互式错误演示">
<ThrowErrorComponent shouldThrow={shouldThrow} errorMessage={errorMessage} />
</ErrorBoundary>
</div>
)
}
}
export const CustomFallback: Story = {
render: () => {
const CustomFallbackComponent = ({ error, onDebugClick, onReloadClick }: CustomFallbackProps) => (
<div className="flex justify-center items-center w-full p-8">
<div className="bg-gradient-to-r from-purple-400 to-pink-400 text-white rounded-lg p-6 text-center">
<h2 className="text-xl font-bold mb-2">😵 </h2>
<p className="mb-4">...</p>
<p className="text-sm opacity-90 mb-4">{error?.message}</p>
<div className="flex gap-2 justify-center">
{onDebugClick && (
<Button size="sm" variant="flat" onPress={onDebugClick}>
</Button>
)}
{onReloadClick && (
<Button size="sm" variant="flat" onPress={onReloadClick}>
</Button>
)}
</div>
</div>
</div>
)
return (
<ErrorBoundary
fallbackComponent={CustomFallbackComponent}
onDebugClick={() => alert('自定义调试')}
onReloadClick={() => alert('自定义重载')}>
<ThrowErrorComponent shouldThrow={true} errorMessage="使用自定义回退组件" />
</ErrorBoundary>
)
}
}
export const NestedErrorBoundaries: Story = {
render: () => (
<div className="space-y-4">
<h3 className="text-lg font-medium"></h3>
<ErrorBoundary errorMessage="外层错误边界">
<div className="p-4 border border-gray-200 dark:border-gray-700 rounded">
<h4 className="font-medium mb-2"></h4>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4"></p>
<ErrorBoundary errorMessage="内层错误边界">
<div className="p-3 bg-gray-50 dark:bg-gray-800 rounded">
<h5 className="font-medium mb-2"></h5>
<ThrowErrorComponent shouldThrow={true} errorMessage="内层组件错误" />
</div>
</ErrorBoundary>
</div>
</ErrorBoundary>
</div>
)
}
export const MultipleComponents: Story = {
render: () => (
<div className="space-y-4">
<h3 className="text-lg font-medium"></h3>
<ErrorBoundary onReloadClick={() => window.location.reload()} reloadButtonText="刷新页面">
<div className="grid grid-cols-2 gap-4">
<ThrowErrorComponent shouldThrow={false} />
<ThrowErrorComponent shouldThrow={false} />
<ThrowErrorComponent shouldThrow={true} errorMessage="其中一个组件出错" />
<ThrowErrorComponent shouldThrow={false} />
</div>
</ErrorBoundary>
</div>
)
}
export const AsyncError: Story = {
render: () => (
<ErrorBoundary
onReloadClick={() => window.location.reload()}
reloadButtonText="重新加载"
errorMessage="异步操作失败">
<AsyncErrorComponent />
</ErrorBoundary>
)
}

View File

@ -0,0 +1,344 @@
import type { Meta, StoryObj } from '@storybook/react'
import IndicatorLight from '../../../src/components/base/IndicatorLight'
const meta: Meta<typeof IndicatorLight> = {
title: 'Base/IndicatorLight',
component: IndicatorLight,
parameters: {
layout: 'centered'
},
tags: ['autodocs'],
argTypes: {
color: {
control: 'color',
description: '指示灯的颜色(支持预设颜色名称或十六进制值)'
},
size: {
control: { type: 'range', min: 4, max: 32, step: 2 },
description: '指示灯的大小(像素)'
},
shadow: {
control: 'boolean',
description: '是否显示发光阴影效果'
},
style: {
control: false,
description: '自定义样式对象'
},
animation: {
control: 'boolean',
description: '是否启用脉冲动画'
}
}
}
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
color: 'green'
}
}
export const Red: Story = {
args: {
color: '#ef4444'
}
}
export const Blue: Story = {
args: {
color: '#3b82f6'
}
}
export const Yellow: Story = {
args: {
color: '#eab308'
}
}
export const Purple: Story = {
args: {
color: '#a855f7'
}
}
export const Orange: Story = {
args: {
color: '#f97316'
}
}
export const WithoutShadow: Story = {
args: {
color: 'green',
shadow: false
}
}
export const WithoutAnimation: Story = {
args: {
color: '#3b82f6',
animation: false
}
}
export const SmallSize: Story = {
args: {
color: '#ef4444',
size: 6
}
}
export const LargeSize: Story = {
args: {
color: '#22c55e',
size: 24
}
}
export const CustomStyle: Story = {
args: {
color: '#8b5cf6',
size: 16,
style: {
border: '2px solid #8b5cf6',
opacity: 0.8
},
className: 'ring-2 ring-purple-200 dark:ring-purple-800'
}
}
export const StatusColors: Story = {
render: () => (
<div className="space-y-6">
<h3 className="text-lg font-medium"></h3>
<div className="grid grid-cols-2 gap-4">
<div className="flex items-center gap-3">
<IndicatorLight color="#22c55e" />
<span>线/</span>
</div>
<div className="flex items-center gap-3">
<IndicatorLight color="#ef4444" />
<span>线/</span>
</div>
<div className="flex items-center gap-3">
<IndicatorLight color="#eab308" />
<span>/</span>
</div>
<div className="flex items-center gap-3">
<IndicatorLight color="#3b82f6" />
<span>/</span>
</div>
<div className="flex items-center gap-3">
<IndicatorLight color="#6b7280" />
<span>/</span>
</div>
<div className="flex items-center gap-3">
<IndicatorLight color="#a855f7" />
<span></span>
</div>
</div>
</div>
)
}
export const SizeComparison: Story = {
render: () => (
<div className="space-y-6">
<h3 className="text-lg font-medium"></h3>
<div className="flex items-center gap-6">
<div className="text-center">
<IndicatorLight color="#22c55e" size={6} />
<p className="text-xs mt-2"> (6px)</p>
</div>
<div className="text-center">
<IndicatorLight color="#22c55e" size={8} />
<p className="text-xs mt-2"> (8px)</p>
</div>
<div className="text-center">
<IndicatorLight color="#22c55e" size={12} />
<p className="text-xs mt-2"> (12px)</p>
</div>
<div className="text-center">
<IndicatorLight color="#22c55e" size={16} />
<p className="text-xs mt-2"> (16px)</p>
</div>
<div className="text-center">
<IndicatorLight color="#22c55e" size={24} />
<p className="text-xs mt-2"> (24px)</p>
</div>
</div>
</div>
)
}
export const UserStatusList: Story = {
render: () => (
<div className="space-y-4">
<h3 className="text-lg font-medium"></h3>
<div className="space-y-3">
<div className="flex items-center gap-3 p-3 border border-gray-200 dark:border-gray-700 rounded">
<IndicatorLight color="#22c55e" size={10} />
<div className="flex-1">
<p className="font-medium"></p>
<p className="text-sm text-gray-500">线 - 5</p>
</div>
</div>
<div className="flex items-center gap-3 p-3 border border-gray-200 dark:border-gray-700 rounded">
<IndicatorLight color="#eab308" size={10} />
<div className="flex-1">
<p className="font-medium"></p>
<p className="text-sm text-gray-500"> - 30</p>
</div>
</div>
<div className="flex items-center gap-3 p-3 border border-gray-200 dark:border-gray-700 rounded">
<IndicatorLight color="#ef4444" size={10} />
<div className="flex-1">
<p className="font-medium"></p>
<p className="text-sm text-gray-500">线 - 2</p>
</div>
</div>
<div className="flex items-center gap-3 p-3 border border-gray-200 dark:border-gray-700 rounded">
<IndicatorLight color="#3b82f6" size={10} />
<div className="flex-1">
<p className="font-medium"></p>
<p className="text-sm text-gray-500"> - </p>
</div>
</div>
</div>
</div>
)
}
export const ServiceStatus: Story = {
render: () => (
<div className="space-y-4">
<h3 className="text-lg font-medium"></h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="p-4 border border-gray-200 dark:border-gray-700 rounded">
<div className="flex items-center justify-between mb-3">
<h4 className="font-medium">Web </h4>
<IndicatorLight color="#22c55e" size={12} />
</div>
<p className="text-sm text-gray-600 dark:text-gray-400">
响应时间: 120ms
<br />
正常运行时间: 99.9%
</p>
</div>
<div className="p-4 border border-gray-200 dark:border-gray-700 rounded">
<div className="flex items-center justify-between mb-3">
<h4 className="font-medium"></h4>
<IndicatorLight color="#eab308" size={12} />
</div>
<p className="text-sm text-gray-600 dark:text-gray-400">
响应时间: 250ms
<br />
正常运行时间: 98.5%
</p>
</div>
<div className="p-4 border border-gray-200 dark:border-gray-700 rounded">
<div className="flex items-center justify-between mb-3">
<h4 className="font-medium">API </h4>
<IndicatorLight color="#22c55e" size={12} />
</div>
<p className="text-sm text-gray-600 dark:text-gray-400">
响应时间: 89ms
<br />
正常运行时间: 99.8%
</p>
</div>
<div className="p-4 border border-gray-200 dark:border-gray-700 rounded">
<div className="flex items-center justify-between mb-3">
<h4 className="font-medium"></h4>
<IndicatorLight color="#ef4444" size={12} />
</div>
<p className="text-sm text-gray-600 dark:text-gray-400">
响应时间: 超时
<br />
正常运行时间: 85.2%
</p>
</div>
</div>
</div>
)
}
export const AnimationComparison: Story = {
render: () => (
<div className="space-y-6">
<h3 className="text-lg font-medium"></h3>
<div className="flex items-center gap-8">
<div className="text-center">
<IndicatorLight color="#22c55e" size={16} animation={true} />
<p className="text-xs mt-2"></p>
</div>
<div className="text-center">
<IndicatorLight color="#22c55e" size={16} animation={false} />
<p className="text-xs mt-2"></p>
</div>
</div>
</div>
)
}
export const NotificationDot: Story = {
render: () => (
<div className="space-y-6">
<h3 className="text-lg font-medium"></h3>
<div className="flex gap-6">
<div className="relative">
<div className="w-12 h-12 bg-gray-200 dark:bg-gray-700 rounded-lg flex items-center justify-center">📧</div>
<div className="absolute -top-1 -right-1">
<IndicatorLight color="#ef4444" size={8} />
</div>
</div>
<div className="relative">
<div className="w-12 h-12 bg-gray-200 dark:bg-gray-700 rounded-lg flex items-center justify-center">🔔</div>
<div className="absolute -top-1 -right-1">
<IndicatorLight color="#ef4444" size={10} />
</div>
</div>
<div className="relative">
<div className="w-12 h-12 bg-gray-200 dark:bg-gray-700 rounded-lg flex items-center justify-center">💬</div>
<div className="absolute -top-1 -right-1">
<IndicatorLight color="#22c55e" size={8} />
</div>
</div>
</div>
</div>
)
}
export const CustomColors: Story = {
render: () => (
<div className="space-y-6">
<h3 className="text-lg font-medium"></h3>
<div className="grid grid-cols-4 gap-4">
{[
'#ff6b6b',
'#4ecdc4',
'#45b7d1',
'#f9ca24',
'#6c5ce7',
'#fd79a8',
'#00b894',
'#e17055',
'#74b9ff',
'#fd79a8',
'#00cec9',
'#fdcb6e'
].map((color, index) => (
<div key={index} className="text-center">
<IndicatorLight color={color} size={14} />
<p className="text-xs mt-2 font-mono">{color}</p>
</div>
))}
</div>
</div>
)
}

View File

@ -0,0 +1,343 @@
import { Button } from '@heroui/react'
import type { Meta, StoryObj } from '@storybook/react'
import { useState } from 'react'
import Spinner from '../../../src/components/base/Spinner'
const meta: Meta<typeof Spinner> = {
title: 'Base/Spinner',
component: Spinner,
parameters: {
layout: 'centered'
},
tags: ['autodocs'],
argTypes: {
text: {
control: false,
description: '加载文字或React节点'
}
}
}
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
text: '加载中...'
}
}
export const ShortText: Story = {
args: {
text: '搜索'
}
}
export const LongText: Story = {
args: {
text: '正在处理您的请求,请稍候'
}
}
export const WithReactNode: Story = {
args: {
text: (
<span>
<strong></strong> ...
</span>
)
}
}
export const CustomStyle: Story = {
args: {
text: '自定义样式',
className: 'bg-blue-50 dark:bg-blue-900/20 px-4 py-2 rounded-lg border border-blue-200 dark:border-blue-700'
}
}
export const LoadingStates: Story = {
render: () => (
<div className="space-y-6">
<h3 className="text-lg font-medium"></h3>
<div className="space-y-4">
<div>
<h4 className="font-medium mb-2"></h4>
<div className="space-y-2">
<Spinner text="正在上传文件..." />
<Spinner text="正在下载文件..." />
<Spinner text="正在压缩文件..." />
</div>
</div>
<div>
<h4 className="font-medium mb-2"></h4>
<div className="space-y-2">
<Spinner text="正在加载数据..." />
<Spinner text="正在保存更改..." />
<Spinner text="正在同步数据..." />
</div>
</div>
<div>
<h4 className="font-medium mb-2"></h4>
<div className="space-y-2">
<Spinner text="正在连接服务器..." />
<Spinner text="正在获取更新..." />
<Spinner text="正在验证账户..." />
</div>
</div>
</div>
</div>
)
}
export const InteractiveDemo: Story = {
render: function InteractiveDemo() {
const [isLoading, setIsLoading] = useState(false)
const [loadingText, setLoadingText] = useState('处理中...')
const handleStartLoading = () => {
setIsLoading(true)
setTimeout(() => {
setIsLoading(false)
}, 3000)
}
return (
<div className="space-y-4">
<div className="flex gap-2">
<Button onPress={handleStartLoading} disabled={isLoading} color="primary">
{isLoading ? '正在处理...' : '开始加载'}
</Button>
<input
type="text"
value={loadingText}
onChange={(e) => setLoadingText(e.target.value)}
placeholder="自定义加载文字"
className="px-3 py-1 border border-gray-300 rounded text-sm"
disabled={isLoading}
/>
</div>
{isLoading && (
<div className="p-4 border border-gray-200 dark:border-gray-700 rounded">
<Spinner text={loadingText} />
</div>
)}
</div>
)
}
}
export const InComponents: Story = {
render: () => (
<div className="space-y-6">
<h3 className="text-lg font-medium"></h3>
<div className="space-y-4">
{/* 搜索框 */}
<div className="space-y-2">
<h4 className="font-medium"></h4>
<div className="relative">
<input
type="text"
placeholder="搜索..."
className="w-full px-4 py-2 pr-32 border border-gray-300 rounded-lg"
/>
<div className="absolute right-2 top-1/2 transform -translate-y-1/2">
<Spinner text="搜索中" />
</div>
</div>
</div>
{/* 按钮加载状态 */}
<div className="space-y-2">
<h4 className="font-medium"></h4>
<div className="flex gap-2">
<Button disabled className="cursor-not-allowed opacity-70">
<Spinner text="保存中..." className="text-sm" />
</Button>
<Button disabled className="cursor-not-allowed opacity-70">
<Spinner text="提交中..." className="text-sm" />
</Button>
</div>
</div>
{/* 卡片加载 */}
<div className="space-y-2">
<h4 className="font-medium"></h4>
<div className="p-6 border border-gray-200 dark:border-gray-700 rounded-lg text-center">
<Spinner text="正在加载内容..." />
</div>
</div>
{/* 列表加载 */}
<div className="space-y-2">
<h4 className="font-medium"></h4>
<div className="space-y-2">
<div className="p-3 border border-gray-200 dark:border-gray-700 rounded">
<p> 1</p>
</div>
<div className="p-3 border border-gray-200 dark:border-gray-700 rounded">
<p> 2</p>
</div>
<div className="p-3 border border-gray-200 dark:border-gray-700 rounded text-center">
<Spinner text="加载更多..." />
</div>
</div>
</div>
</div>
</div>
)
}
export const DifferentSizes: Story = {
render: () => (
<div className="space-y-6">
<h3 className="text-lg font-medium"></h3>
<div className="space-y-4">
<div className="flex items-center gap-4">
<span className="w-20 text-sm">:</span>
<Spinner text="加载" className="text-xs" />
</div>
<div className="flex items-center gap-4">
<span className="w-20 text-sm">:</span>
<Spinner text="加载中..." />
</div>
<div className="flex items-center gap-4">
<span className="w-20 text-sm">:</span>
<Spinner text="正在处理大量数据..." className="text-lg" />
</div>
</div>
</div>
)
}
export const ColorVariations: Story = {
render: () => (
<div className="space-y-6">
<h3 className="text-lg font-medium"></h3>
<div className="space-y-4">
<div className="space-y-2">
<Spinner text="默认颜色" />
<Spinner text="蓝色主题" className="text-blue-600 dark:text-blue-400" />
<Spinner text="绿色成功" className="text-green-600 dark:text-green-400" />
<Spinner text="橙色警告" className="text-orange-600 dark:text-orange-400" />
<Spinner text="红色错误" className="text-red-600 dark:text-red-400" />
<Spinner text="紫色特殊" className="text-purple-600 dark:text-purple-400" />
</div>
</div>
</div>
)
}
export const BackgroundVariations: Story = {
render: () => (
<div className="space-y-6">
<h3 className="text-lg font-medium"></h3>
<div className="space-y-4">
<div className="p-4 bg-white dark:bg-gray-800 rounded border">
<Spinner text="白色背景" />
</div>
<div className="p-4 bg-gray-100 dark:bg-gray-700 rounded">
<Spinner text="灰色背景" />
</div>
<div className="p-4 bg-blue-500 text-white rounded">
<Spinner text="蓝色背景" className="text-white" />
</div>
<div className="p-4 bg-green-500 text-white rounded">
<Spinner text="绿色背景" className="text-white" />
</div>
</div>
</div>
)
}
export const LoadingSequence: Story = {
render: function LoadingSequence() {
const [step, setStep] = useState(0)
const steps = ['准备中...', '连接服务器...', '验证身份...', '加载数据...', '处理结果...', '完成!']
const nextStep = () => {
setStep((prev) => (prev + 1) % steps.length)
}
const currentStep = steps[step]
const isComplete = step === steps.length - 1
return (
<div className="space-y-4">
<Button onPress={nextStep}>{isComplete ? '重新开始' : '下一步'}</Button>
<div className="p-6 border border-gray-200 dark:border-gray-700 rounded-lg">
{isComplete ? (
<div className="text-center text-green-600 dark:text-green-400 font-medium"> {currentStep}</div>
) : (
<Spinner text={currentStep} />
)}
</div>
<div className="text-sm text-gray-500">
{step + 1} / {steps.length}
</div>
</div>
)
}
}
export const RealWorldUsage: Story = {
render: () => (
<div className="space-y-6">
<h3 className="text-lg font-medium"></h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* 表单提交 */}
<div className="p-4 border border-gray-200 dark:border-gray-700 rounded">
<h4 className="font-medium mb-3"></h4>
<div className="space-y-3">
<input type="email" placeholder="邮箱" className="w-full px-3 py-2 border border-gray-300 rounded" />
<input type="password" placeholder="密码" className="w-full px-3 py-2 border border-gray-300 rounded" />
<div className="text-center">
<Spinner text="正在登录..." />
</div>
</div>
</div>
{/* 文件上传 */}
<div className="p-4 border border-gray-200 dark:border-gray-700 rounded">
<h4 className="font-medium mb-3"></h4>
<div className="border-2 border-dashed border-gray-300 rounded-lg p-6 text-center">
<Spinner text="上传中 (75%)" />
<div className="mt-2 w-full bg-gray-200 rounded-full h-2">
<div className="bg-blue-600 h-2 rounded-full" style={{ width: '75%' }}></div>
</div>
</div>
</div>
{/* 数据获取 */}
<div className="p-4 border border-gray-200 dark:border-gray-700 rounded">
<h4 className="font-medium mb-3"></h4>
<div className="space-y-2">
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded animate-pulse w-3/4"></div>
<div className="text-center mt-4">
<Spinner text="获取最新数据..." />
</div>
</div>
</div>
{/* 页面切换 */}
<div className="p-4 border border-gray-200 dark:border-gray-700 rounded">
<h4 className="font-medium mb-3"></h4>
<div className="text-center">
<div className="h-32 bg-gray-100 dark:bg-gray-700 rounded flex items-center justify-center">
<Spinner text="加载页面..." />
</div>
</div>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,383 @@
import type { Meta, StoryObj } from '@storybook/react'
import TextBadge from '../../../src/components/base/TextBadge'
const meta: Meta<typeof TextBadge> = {
title: 'Base/TextBadge',
component: TextBadge,
parameters: {
layout: 'centered'
},
tags: ['autodocs'],
argTypes: {
text: {
control: 'text',
description: '徽章显示的文字'
},
style: {
control: false,
description: '自定义样式对象'
}
}
}
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
text: '新'
}
}
export const ShortText: Story = {
args: {
text: 'V2'
}
}
export const LongText: Story = {
args: {
text: '热门推荐'
}
}
export const Numbers: Story = {
args: {
text: '99+'
}
}
export const Status: Story = {
args: {
text: '已完成'
}
}
export const Version: Story = {
args: {
text: 'v1.2.0'
}
}
export const CustomStyle: Story = {
args: {
text: '自定义',
style: {
backgroundColor: '#10b981',
color: 'white',
fontSize: '11px'
}
}
}
export const CustomClassName: Story = {
args: {
text: '特殊样式',
className:
'bg-purple-100 dark:bg-purple-900/30 text-purple-600 dark:text-purple-400 border border-purple-200 dark:border-purple-700'
}
}
export const ColorVariations: Story = {
render: () => (
<div className="space-y-6">
<h3 className="text-lg font-medium"></h3>
<div className="flex flex-wrap gap-3">
<TextBadge text="默认蓝色" />
<TextBadge text="绿色" className="bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400" />
<TextBadge text="红色" className="bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400" />
<TextBadge text="黄色" className="bg-yellow-100 dark:bg-yellow-900/30 text-yellow-600 dark:text-yellow-400" />
<TextBadge text="紫色" className="bg-purple-100 dark:bg-purple-900/30 text-purple-600 dark:text-purple-400" />
<TextBadge text="灰色" className="bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400" />
</div>
</div>
)
}
export const StatusBadges: Story = {
render: () => (
<div className="space-y-6">
<h3 className="text-lg font-medium"></h3>
<div className="space-y-4">
<div>
<h4 className="font-medium mb-2"></h4>
<div className="flex flex-wrap gap-2">
<TextBadge text="待处理" className="bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400" />
<TextBadge text="进行中" className="bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400" />
<TextBadge text="已完成" className="bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400" />
<TextBadge text="已取消" className="bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400" />
</div>
</div>
<div>
<h4 className="font-medium mb-2"></h4>
<div className="flex flex-wrap gap-2">
<TextBadge text="低" className="bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400" />
<TextBadge text="中" />
<TextBadge text="高" className="bg-orange-100 dark:bg-orange-900/30 text-orange-600 dark:text-orange-400" />
<TextBadge text="紧急" className="bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400" />
</div>
</div>
<div>
<h4 className="font-medium mb-2"></h4>
<div className="flex flex-wrap gap-2">
<TextBadge text="功能" />
<TextBadge text="修复" className="bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400" />
<TextBadge text="优化" className="bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400" />
<TextBadge
text="文档"
className="bg-purple-100 dark:bg-purple-900/30 text-purple-600 dark:text-purple-400"
/>
</div>
</div>
</div>
</div>
)
}
export const InUserInterface: Story = {
render: () => (
<div className="space-y-6">
<h3 className="text-lg font-medium"></h3>
<div className="space-y-4">
{/* 导航菜单 */}
<div>
<h4 className="font-medium mb-2"></h4>
<nav className="flex gap-4">
<div className="flex items-center gap-2">
<span></span>
</div>
<div className="flex items-center gap-2">
<span></span>
<TextBadge text="新" />
</div>
<div className="flex items-center gap-2">
<span></span>
<TextBadge text="5" className="bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400" />
</div>
<div className="flex items-center gap-2">
<span></span>
</div>
</nav>
</div>
{/* 卡片列表 */}
<div>
<h4 className="font-medium mb-2"></h4>
<div className="space-y-3">
<div className="p-4 border border-gray-200 dark:border-gray-700 rounded">
<div className="flex items-start justify-between">
<div className="flex-1">
<h5 className="font-medium">React 18 </h5>
<p className="text-sm text-gray-500 mt-1"> React 18 ...</p>
</div>
<div className="flex gap-2 ml-4">
<TextBadge text="前端" />
<TextBadge
text="推荐"
className="bg-yellow-100 dark:bg-yellow-900/30 text-yellow-600 dark:text-yellow-400"
/>
</div>
</div>
</div>
<div className="p-4 border border-gray-200 dark:border-gray-700 rounded">
<div className="flex items-start justify-between">
<div className="flex-1">
<h5 className="font-medium">Node.js </h5>
<p className="text-sm text-gray-500 mt-1"> Node.js ...</p>
</div>
<div className="flex gap-2 ml-4">
<TextBadge text="后端" />
<TextBadge text="热门" className="bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400" />
</div>
</div>
</div>
<div className="p-4 border border-gray-200 dark:border-gray-700 rounded">
<div className="flex items-start justify-between">
<div className="flex-1">
<h5 className="font-medium">TypeScript </h5>
<p className="text-sm text-gray-500 mt-1">TypeScript ...</p>
</div>
<div className="flex gap-2 ml-4">
<TextBadge text="TypeScript" />
<TextBadge text="新" />
</div>
</div>
</div>
</div>
</div>
{/* 用户列表 */}
<div>
<h4 className="font-medium mb-2"></h4>
<div className="space-y-2">
<div className="flex items-center justify-between p-3 border border-gray-200 dark:border-gray-700 rounded">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-blue-500 text-white rounded-full flex items-center justify-center text-sm">
</div>
<div>
<p className="font-medium"></p>
<p className="text-sm text-gray-500"></p>
</div>
</div>
<div className="flex gap-2">
<TextBadge
text="管理员"
className="bg-purple-100 dark:bg-purple-900/30 text-purple-600 dark:text-purple-400"
/>
<TextBadge
text="在线"
className="bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400"
/>
</div>
</div>
<div className="flex items-center justify-between p-3 border border-gray-200 dark:border-gray-700 rounded">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-green-500 text-white rounded-full flex items-center justify-center text-sm">
</div>
<div>
<p className="font-medium"></p>
<p className="text-sm text-gray-500"></p>
</div>
</div>
<div className="flex gap-2">
<TextBadge text="成员" />
<TextBadge text="离线" className="bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400" />
</div>
</div>
</div>
</div>
</div>
</div>
)
}
export const VersionTags: Story = {
render: () => (
<div className="space-y-6">
<h3 className="text-lg font-medium"></h3>
<div className="space-y-4">
<div>
<h4 className="font-medium mb-2"></h4>
<div className="flex flex-wrap gap-2">
<TextBadge text="v1.0.0" />
<TextBadge text="v1.1.0" />
<TextBadge text="v2.0.0-beta" />
<TextBadge text="v2.1.0" className="bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400" />
<TextBadge text="latest" className="bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400" />
</div>
</div>
<div>
<h4 className="font-medium mb-2"></h4>
<div className="flex flex-wrap gap-2">
<TextBadge text="开发" className="bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400" />
<TextBadge
text="测试"
className="bg-yellow-100 dark:bg-yellow-900/30 text-yellow-600 dark:text-yellow-400"
/>
<TextBadge
text="预发布"
className="bg-orange-100 dark:bg-orange-900/30 text-orange-600 dark:text-orange-400"
/>
<TextBadge text="生产" className="bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400" />
</div>
</div>
</div>
</div>
)
}
export const NumberBadges: Story = {
render: () => (
<div className="space-y-6">
<h3 className="text-lg font-medium"></h3>
<div className="space-y-4">
<div>
<h4 className="font-medium mb-2"></h4>
<div className="flex flex-wrap gap-4">
<div className="flex items-center gap-2">
<span></span>
<TextBadge text="3" className="bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400" />
</div>
<div className="flex items-center gap-2">
<span></span>
<TextBadge text="12" className="bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400" />
</div>
<div className="flex items-center gap-2">
<span></span>
<TextBadge text="99+" className="bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400" />
</div>
</div>
</div>
<div>
<h4 className="font-medium mb-2"></h4>
<div className="flex flex-wrap gap-4">
<div className="flex items-center gap-2">
<span>访</span>
<TextBadge text="1.2K" />
</div>
<div className="flex items-center gap-2">
<span></span>
<TextBadge text="856" />
</div>
<div className="flex items-center gap-2">
<span></span>
<TextBadge text="234" />
</div>
</div>
</div>
</div>
</div>
)
}
export const SizeVariations: Story = {
render: () => (
<div className="space-y-6">
<h3 className="text-lg font-medium"></h3>
<div className="flex items-center gap-4">
<TextBadge text="超小" style={{ fontSize: '10px', padding: '1px 4px' }} />
<TextBadge text="小" style={{ fontSize: '11px', padding: '2px 6px' }} />
<TextBadge text="默认" />
<TextBadge text="大" style={{ fontSize: '14px', padding: '4px 8px' }} />
<TextBadge text="超大" style={{ fontSize: '16px', padding: '6px 12px' }} />
</div>
</div>
)
}
export const OutlineBadges: Story = {
render: () => (
<div className="space-y-6">
<h3 className="text-lg font-medium"></h3>
<div className="flex flex-wrap gap-3">
<TextBadge
text="边框"
className="bg-transparent border border-blue-600 text-blue-600 dark:border-blue-400 dark:text-blue-400"
/>
<TextBadge
text="绿色边框"
className="bg-transparent border border-green-600 text-green-600 dark:border-green-400 dark:text-green-400"
/>
<TextBadge
text="红色边框"
className="bg-transparent border border-red-600 text-red-600 dark:border-red-400 dark:text-red-400"
/>
<TextBadge
text="虚线边框"
className="bg-transparent border border-dashed border-purple-600 text-purple-600 dark:border-purple-400 dark:text-purple-400"
/>
</div>
</div>
)
}

View File

@ -0,0 +1,173 @@
import type { Meta, StoryObj } from '@storybook/react'
import EmojiAvatar from '../../../src/components/display/EmojiAvatar'
const meta: Meta<typeof EmojiAvatar> = {
title: 'Display/EmojiAvatar',
component: EmojiAvatar,
parameters: {
layout: 'centered'
},
tags: ['autodocs'],
argTypes: {
children: {
control: 'text',
description: 'Emoji 字符',
defaultValue: '😊'
},
size: {
control: { type: 'range', min: 20, max: 100, step: 1 },
description: '头像尺寸',
defaultValue: 31
},
fontSize: {
control: { type: 'range', min: 10, max: 50, step: 1 },
description: '字体大小(默认为 size * 0.5'
},
className: {
control: 'text',
description: '自定义类名'
}
}
} satisfies Meta<typeof EmojiAvatar>
export default meta
type Story = StoryObj<typeof meta>
// 基础用法
export const Default: Story = {
args: {
children: '😊',
size: 40
}
}
// 不同尺寸展示
export const Sizes: Story = {
render: (args) => (
<div className="flex items-center gap-4">
<EmojiAvatar {...args} children="😊" size={24} />
<EmojiAvatar {...args} children="🎉" size={32} />
<EmojiAvatar {...args} children="🚀" size={40} />
<EmojiAvatar {...args} children="❤️" size={48} />
<EmojiAvatar {...args} children="🌟" size={56} />
<EmojiAvatar {...args} children="🎨" size={64} />
</div>
)
}
// 各种 Emoji
export const VariousEmojis: Story = {
render: (args) => (
<div className="grid grid-cols-6 gap-4">
{['😀', '😎', '🥳', '🤔', '😴', '🤯',
'❤️', '🔥', '✨', '🎉', '🎯', '🚀',
'🌟', '🌈', '☀️', '🌸', '🍕', '🎨',
'📚', '💡', '🔧', '🎮', '🎵', '🏆'].map((emoji) => (
<EmojiAvatar key={emoji} {...args} children={emoji} size={40} />
))}
</div>
)
}
// 自定义字体大小
export const CustomFontSize: Story = {
render: (args) => (
<div className="flex items-center gap-4">
<div className="text-center">
<EmojiAvatar {...args} children="🎯" size={50} fontSize={15} />
<p className="mt-2 text-xs text-gray-500">字体: 15px</p>
</div>
<div className="text-center">
<EmojiAvatar {...args} children="🎯" size={50} fontSize={25} />
<p className="mt-2 text-xs text-gray-500">字体: 25px ()</p>
</div>
<div className="text-center">
<EmojiAvatar {...args} children="🎯" size={50} fontSize={35} />
<p className="mt-2 text-xs text-gray-500">字体: 35px</p>
</div>
</div>
)
}
// 点击交互
export const Interactive: Story = {
args: {
children: '👆',
size: 50,
onClick: () => alert('Emoji clicked!')
}
}
// 自定义样式
export const CustomStyles: Story = {
render: (args) => (
<div className="flex items-center gap-4">
<EmojiAvatar
{...args}
children="🎨"
size={50}
style={{ backgroundColor: '#ffe4e1' }}
/>
<EmojiAvatar
{...args}
children="🌊"
size={50}
style={{ backgroundColor: '#e0f2ff' }}
/>
<EmojiAvatar
{...args}
children="🌿"
size={50}
style={{ backgroundColor: '#e8f5e9' }}
/>
<EmojiAvatar
{...args}
children="☀️"
size={50}
style={{ backgroundColor: '#fff8e1' }}
/>
</div>
)
}
// 组合使用
export const WithLabels: Story = {
render: (args) => (
<div className="flex items-center gap-6">
{[
{ emoji: '😊', label: 'Happy' },
{ emoji: '😢', label: 'Sad' },
{ emoji: '😡', label: 'Angry' },
{ emoji: '😴', label: 'Tired' }
].map(({ emoji, label }) => (
<div key={label} className="flex flex-col items-center gap-2">
<EmojiAvatar {...args} children={emoji} size={48} />
<span className="text-sm text-gray-600">{label}</span>
</div>
))}
</div>
)
}
// 网格展示
export const Grid: Story = {
render: (args) => (
<div className="w-96">
<h3 className="mb-4 text-lg font-semibold"></h3>
<div className="grid grid-cols-8 gap-2">
{['😊', '😂', '😍', '🤔', '😎', '😴', '😭', '😡',
'🤗', '😏', '😅', '😌', '🙄', '😮', '😐', '😯',
'😪', '😫', '🥱', '😤', '😢', '😥', '😰', '🤯'].map((emoji) => (
<EmojiAvatar
key={emoji}
{...args}
children={emoji}
size={36}
onClick={() => console.log(`Selected: ${emoji}`)}
/>
))}
</div>
</div>
)
}

View File

@ -0,0 +1,199 @@
import type { Meta, StoryObj } from '@storybook/react'
import ExpandableText from '../../../src/components/display/ExpandableText'
const meta: Meta<typeof ExpandableText> = {
title: 'Display/ExpandableText',
component: ExpandableText,
parameters: {
layout: 'centered'
},
tags: ['autodocs'],
argTypes: {
text: {
control: 'text',
description: '要显示的文本内容'
},
expandText: {
control: 'text',
description: '展开按钮文本',
defaultValue: 'Expand'
},
collapseText: {
control: 'text',
description: '收起按钮文本',
defaultValue: 'Collapse'
},
lineClamp: {
control: { type: 'range', min: 1, max: 5, step: 1 },
description: '收起时显示的行数',
defaultValue: 1
},
className: {
control: 'text',
description: '自定义类名'
}
}
} satisfies Meta<typeof ExpandableText>
export default meta
type Story = StoryObj<typeof meta>
const longText = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.'
const chineseText = '这是一段很长的中文文本内容,用于测试文本展开和收起功能。当文本内容超过指定的行数限制时,会显示省略号,用户可以点击展开按钮查看完整内容,也可以点击收起按钮将文本重新收起。这个组件在显示长文本内容时非常有用。'
// 基础用法
export const Default: Story = {
args: {
text: longText,
expandText: 'Expand',
collapseText: 'Collapse'
}
}
// 单行省略
export const SingleLine: Story = {
args: {
text: longText,
lineClamp: 1
}
}
// 多行省略
export const MultiLine: Story = {
render: (args) => (
<div className="flex flex-col gap-4 w-96">
<div>
<h3 className="text-sm font-semibold mb-2"> 2 </h3>
<ExpandableText {...args} text={longText} lineClamp={2} />
</div>
<div>
<h3 className="text-sm font-semibold mb-2"> 3 </h3>
<ExpandableText {...args} text={longText} lineClamp={3} />
</div>
<div>
<h3 className="text-sm font-semibold mb-2"> 4 </h3>
<ExpandableText {...args} text={longText} lineClamp={4} />
</div>
</div>
)
}
// 中文文本
export const ChineseText: Story = {
args: {
text: chineseText,
expandText: '展开',
collapseText: '收起',
lineClamp: 2
}
}
// 短文本(不需要展开)
export const ShortText: Story = {
args: {
text: 'This is a short text.',
lineClamp: 1
}
}
// 自定义按钮文本
export const CustomButtonText: Story = {
args: {
text: longText,
expandText: 'Show More',
collapseText: 'Show Less',
lineClamp: 2
}
}
// 不同语言示例
export const Multilingual: Story = {
render: (args) => (
<div className="flex flex-col gap-6 w-96">
<div>
<h3 className="text-sm font-semibold mb-2">English</h3>
<ExpandableText
{...args}
text="This is a long English text that demonstrates the expand and collapse functionality. It contains multiple sentences to show how the component handles longer content."
expandText="Read more"
collapseText="Read less"
lineClamp={2}
/>
</div>
<div>
<h3 className="text-sm font-semibold mb-2"></h3>
<ExpandableText
{...args}
text="这是一段较长的中文示例文本,用于展示组件的展开和收起功能。它包含多个句子,以显示组件如何处理较长的内容。"
expandText="查看更多"
collapseText="收起"
lineClamp={2}
/>
</div>
<div>
<h3 className="text-sm font-semibold mb-2"></h3>
<ExpandableText
{...args}
text="これは、展開と折りたたみ機能を示す長い日本語のテキストです。コンポーネントが長いコンテンツをどのように処理するかを示すために、複数の文が含まれています。"
expandText="もっと見る"
collapseText="閉じる"
lineClamp={2}
/>
</div>
</div>
)
}
// 在卡片中使用
export const InCard: Story = {
render: (args) => (
<div className="bg-white rounded-lg shadow-md p-6 max-w-md">
<h2 className="text-xl font-bold mb-2">Article Title</h2>
<p className="text-sm text-gray-500 mb-4">Published on December 1, 2024</p>
<ExpandableText
{...args}
text="This is a preview of the article content. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris."
lineClamp={3}
/>
</div>
)
}
// 列表项中使用
export const InList: Story = {
render: (args) => (
<div className="space-y-4 max-w-lg">
{[
{
title: 'First Item',
text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore.'
},
{
title: 'Second Item',
text: 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.'
},
{
title: 'Third Item',
text: 'Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.'
}
].map((item, index) => (
<div key={index} className="border rounded-lg p-4">
<h3 className="font-semibold mb-2">{item.title}</h3>
<ExpandableText {...args} text={item.text} lineClamp={2} />
</div>
))}
</div>
)
}
// 自定义样式
export const CustomStyle: Story = {
args: {
text: longText,
lineClamp: 2,
className: 'bg-blue-50 p-4 rounded-lg',
style: { fontStyle: 'italic' }
}
}

View File

@ -0,0 +1,210 @@
import type { Meta, StoryObj } from '@storybook/react'
import { ProviderAvatar } from '../../../src/components/display/ProviderAvatar'
// 定义 Story 的元数据
const meta: Meta<typeof ProviderAvatar> = {
title: 'Display/ProviderAvatar',
component: ProviderAvatar,
parameters: {
layout: 'centered'
},
tags: ['autodocs'],
argTypes: {
size: {
control: { type: 'range', min: 16, max: 128, step: 4 },
description: '头像尺寸',
defaultValue: 40
},
providerId: {
control: 'text',
description: '提供商 ID'
},
providerName: {
control: 'text',
description: '提供商名称'
},
logoSrc: {
control: 'text',
description: '图片 Logo 地址'
},
className: {
control: 'text',
description: '自定义类名'
}
}
} satisfies Meta<typeof ProviderAvatar>
export default meta
type Story = StoryObj<typeof meta>
// 基础用法:文字头像
export const Default: Story = {
args: {
providerId: 'openai',
providerName: 'OpenAI',
size: 40
}
}
// 带图片的头像
export const WithImage: Story = {
args: {
providerId: 'custom',
providerName: 'Custom Provider',
logoSrc: 'https://via.placeholder.com/150',
size: 40
}
}
// 不同尺寸展示
export const Sizes: Story = {
render: (args) => (
<div className="flex items-center gap-4">
<ProviderAvatar {...args} providerName="Small" size="sm" />
<ProviderAvatar {...args} providerName="Medium" size="md" />
<ProviderAvatar {...args} providerName="Large" size="lg" />
<ProviderAvatar {...args} providerName="24px" size={24} />
<ProviderAvatar {...args} providerName="48px" size={48} />
<ProviderAvatar {...args} providerName="72px" size={72} />
</div>
),
args: {
providerId: 'size-demo'
}
}
// 不同首字母的颜色生成
export const ColorGeneration: Story = {
args: {
providerId: 'azure',
providerName: 'Azure',
size: 40
},
render: (args) => (
<div className="flex items-center gap-4">
<ProviderAvatar {...args} providerId="azure" providerName="Azure" size={40} />
<ProviderAvatar {...args} providerId="anthropic" providerName="Anthropic" size={40} />
<ProviderAvatar {...args} providerId="baidu" providerName="Baidu" size={40} />
<ProviderAvatar {...args} providerId="google" providerName="Google" size={40} />
<ProviderAvatar {...args} providerId="meta" providerName="Meta" size={40} />
<ProviderAvatar {...args} providerId="openai" providerName="OpenAI" size={40} />
<ProviderAvatar {...args} providerId="perplexity" providerName="Perplexity" size={40} />
<ProviderAvatar {...args} providerId="zhipu" providerName="智谱" size={40} />
<ProviderAvatar {...args} providerId="alibaba" providerName="阿里云" size={40} />
<ProviderAvatar {...args} providerId="tencent" providerName="腾讯云" size={40} />
</div>
)
}
// 自定义 SVG Logo
export const WithCustomSvg: Story = {
args: {
providerId: 'custom-svg',
providerName: 'Custom SVG',
size: 40,
renderCustomLogo: () => (
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2L2 7L12 12L22 7L12 2Z" />
<path d="M2 17L12 22L22 17L12 12L2 17Z" />
<path d="M2 12L12 17L22 12L12 7L2 12Z" />
</svg>
)
}
}
// 混合展示
export const Mixed: Story = {
args: {
providerId: 'text',
providerName: 'Text Avatar',
size: 40
},
render: (args) => (
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
<ProviderAvatar {...args} providerId="text" providerName="Text Avatar" size={40} />
<ProviderAvatar
{...args}
providerId="image"
providerName="Image Avatar"
logoSrc="https://via.placeholder.com/150/0000FF/FFFFFF?text=IMG"
size={40}
/>
<ProviderAvatar
{...args}
providerId="svg"
providerName="SVG Avatar"
size={40}
renderCustomLogo={() => (
<svg viewBox="0 0 24 24" fill="#FF6B6B">
<circle cx="12" cy="12" r="10" />
</svg>
)}
/>
</div>
)
}
// 空值处理
export const EmptyValues: Story = {
args: {
providerId: 'empty',
providerName: '',
size: 40
},
render: (args) => (
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
<div>
<p style={{ margin: '0 0 8px 0', fontSize: '12px', color: '#666' }}></p>
<ProviderAvatar {...args} providerId="empty" providerName="" size={40} />
</div>
<div>
<p style={{ margin: '0 0 8px 0', fontSize: '12px', color: '#666' }}></p>
<ProviderAvatar {...args} providerId="normal" providerName="Normal" size={40} />
</div>
</div>
)
}
// 自定义样式
export const CustomStyle: Story = {
args: {
providerId: 'custom-style',
providerName: 'Custom',
size: 40,
style: {
border: '2px solid #FF6B6B',
boxShadow: '0 4px 8px rgba(0,0,0,0.1)'
}
}
}
// 响应式网格展示
export const ResponsiveGrid: Story = {
args: {
providerId: 'provider-a',
providerName: 'Provider A',
size: 48
},
render: (args) => (
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(60px, 1fr))',
gap: '16px',
width: '400px'
}}>
{['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L'].map((letter) => (
<div key={letter} style={{ textAlign: 'center' }}>
<ProviderAvatar
{...args}
providerId={`provider-${letter.toLowerCase()}`}
providerName={`Provider ${letter}`}
size={48}
/>
<div style={{ fontSize: '12px', marginTop: '4px', color: '#666' }}>{letter}</div>
</div>
))}
</div>
)
}

View File

@ -19,5 +19,5 @@
"target": "ES2020"
},
"exclude": ["node_modules", "dist", "**/*.test.*", "**/__tests__/**"],
"include": ["src/**/*"]
"include": ["src/**/*", "stories/**/*"]
}