mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-26 03:31:24 +08:00
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:
parent
d397a43806
commit
f83c3e171e
@ -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 渲染器 |
|
||||
|
||||
@ -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 |
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
`
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 }
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -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(() => {
|
||||
|
||||
123
packages/ui/stories/components/base/CopyButton.stories.tsx
Normal file
123
packages/ui/stories/components/base/CopyButton.stories.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
284
packages/ui/stories/components/base/CustomCollapse.stories.tsx
Normal file
284
packages/ui/stories/components/base/CustomCollapse.stories.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
214
packages/ui/stories/components/base/DividerWithText.stories.tsx
Normal file
214
packages/ui/stories/components/base/DividerWithText.stories.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
310
packages/ui/stories/components/base/EmojiIcon.stories.tsx
Normal file
310
packages/ui/stories/components/base/EmojiIcon.stories.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
249
packages/ui/stories/components/base/ErrorBoundary.stories.tsx
Normal file
249
packages/ui/stories/components/base/ErrorBoundary.stories.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
344
packages/ui/stories/components/base/IndicatorLight.stories.tsx
Normal file
344
packages/ui/stories/components/base/IndicatorLight.stories.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
343
packages/ui/stories/components/base/Spinner.stories.tsx
Normal file
343
packages/ui/stories/components/base/Spinner.stories.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
383
packages/ui/stories/components/base/TextBadge.stories.tsx
Normal file
383
packages/ui/stories/components/base/TextBadge.stories.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
173
packages/ui/stories/components/display/EmojiAvatar.stories.tsx
Normal file
173
packages/ui/stories/components/display/EmojiAvatar.stories.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@ -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' }
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
@ -19,5 +19,5 @@
|
||||
"target": "ES2020"
|
||||
},
|
||||
"exclude": ["node_modules", "dist", "**/*.test.*", "**/__tests__/**"],
|
||||
"include": ["src/**/*"]
|
||||
"include": ["src/**/*", "stories/**/*"]
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user