Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/skeleton-animation-api-refresh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@tiny-design/react": major
"@tiny-design/tokens": major
---

Redesign Skeleton animation APIs, component structure, and related configuration.
9 changes: 7 additions & 2 deletions packages/react/src/config-provider/config-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,19 @@ import React from 'react';
import { SizeType } from '../_utils/props';
import { SpaceSize } from '../space/types';
import { Locale } from '../locale/types';
import { SkeletonAnimation } from '../skeleton/types';
import { ThemeConfig } from './token-utils';

export type ThemeMode = 'light' | 'dark' | 'system';

export interface SkeletonConfig {
animation?: SkeletonAnimation;
}

export interface ConfigContextProps {
prefixCls?: string;
componentSize?: SizeType;
shimmer?: boolean;
skeleton?: SkeletonConfig;
space?: SpaceSize;
theme?: ThemeMode;
themeConfig?: ThemeConfig;
Expand All @@ -21,7 +26,7 @@ export interface ConfigContextProps {
export const ConfigContext = React.createContext<ConfigContextProps>({
prefixCls: 'ty',
componentSize: 'md',
shimmer: false,
skeleton: undefined,
space: 'sm',
getPopupContainer: () => document.body,
getTargetContainer: () => window,
Expand Down
9 changes: 6 additions & 3 deletions packages/react/src/config-provider/config-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const ConfigProviderImpl = (props: ConfigProviderProps): React.ReactElement => {
locale,
prefixCls,
componentSize,
shimmer,
skeleton,
space,
getPopupContainer,
getTargetContainer,
Expand Down Expand Up @@ -49,7 +49,10 @@ const ConfigProviderImpl = (props: ConfigProviderProps): React.ReactElement => {
() => ({
prefixCls: prefixCls ?? parentConfig.prefixCls,
componentSize: componentSize ?? parentConfig.componentSize,
shimmer: shimmer ?? parentConfig.shimmer,
skeleton: {
...parentConfig.skeleton,
...skeleton,
},
space: space ?? parentConfig.space,
theme: mode ?? parentConfig.theme,
themeConfig: themeConfig ?? parentConfig.themeConfig,
Expand All @@ -65,7 +68,7 @@ const ConfigProviderImpl = (props: ConfigProviderProps): React.ReactElement => {
mode,
parentConfig,
prefixCls,
shimmer,
skeleton,
space,
themeConfig,
]
Expand Down
4 changes: 2 additions & 2 deletions packages/react/src/config-provider/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,8 +226,8 @@ const { componentSize, getPopupContainer, locale } = ConfigProvider.useConfig();
| ----------------- | ------------------------------------------------------------- | ------------------------------------------------- | --------- |
| prefixCls | set prefix class. | string | ty |
| componentSize | component size. | enum: `lg` &#124; `md` &#124; `sm` | `md` |
| shimmer | display shimmer effect for [Skeleton](#/components/skeleton). | boolean | false |
| space | set Space size, ref [Space](#/components/space). | enum: `sm` &#124; `md` &#124; `lg` or `number`. | `sm` |
| skeleton | global config for [Skeleton](../components/skeleton), such as animation. | `{ animation?: false \| 'pulse' \| 'shimmer' }` | - |
| space | set Space size, ref [Space](../components/space). | enum: `sm` &#124; `md` &#124; `lg` or `number`. | `sm` |
| locale | set locale for components (e.g. `en_US`, `zh_CN`). | Locale | - |
| getPopupContainer | set the container for popup-based components within this provider scope. | `(trigger?: HTMLElement \| null) => HTMLElement` | provider popup holder |
| getTargetContainer | set the default scroll target for components such as `Anchor`, `Sticky`, and `BackTop`, and the scroll-lock target for layers such as `Overlay` and `Tour`. | `() => HTMLElement \| Window` | `() => window` |
Expand Down
4 changes: 2 additions & 2 deletions packages/react/src/config-provider/index.zh_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,8 +226,8 @@ const { componentSize, getPopupContainer, locale } = ConfigProvider.useConfig();
| ----------------- | ------------------------------------------------------------- | ------------------------------------------------- | --------- |
| prefixCls | 设置类名前缀 | string | ty |
| componentSize | 组件大小 | enum: `lg` &#124; `md` &#124; `sm` | `md` |
| shimmer | 为 [Skeleton](#/components/skeleton) 显示微光动画效果 | boolean | false |
| space | 设置 Space 间距,参考 [Space](#/components/space) | enum: `sm` &#124; `md` &#124; `lg` or `number`. | `sm` |
| skeleton | 为 [Skeleton](../components/skeleton) 提供全局配置,例如动画效果 | `{ animation?: false \| 'pulse' \| 'shimmer' }` | - |
| space | 设置 Space 间距,参考 [Space](../components/space) | enum: `sm` &#124; `md` &#124; `lg` or `number`. | `sm` |
| locale | 设置组件语言包(如 `en_US`、`zh_CN`) | Locale | - |
| getPopupContainer | 为当前 provider 作用域内的弹层组件指定挂载容器 | `(trigger?: HTMLElement \| null) => HTMLElement` | provider popup holder |
| getTargetContainer | 为 `Anchor`、`Sticky`、`BackTop` 等组件设置默认滚动容器,同时为 `Overlay`、`Tour` 这类层级组件设置滚动锁目标 | `() => HTMLElement \| Window` | `() => window` |
Expand Down
2 changes: 1 addition & 1 deletion packages/react/src/image/demo/Placeholder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export default function PlaceholderDemo() {
height={180}
src={src}
alt="Mountain lake"
placeholder={<Skeleton active style={{ width: '100%', height: '100%' }} />}
placeholder={<Skeleton animation="shimmer" width="100%" height="100%" />}
/>
</Flex>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@ exports[`<Skeleton /> should match the snapshot 1`] = `
<DocumentFragment>
<div
aria-busy="true"
class="ty-skeleton"
role="status"
/>
>
<div
aria-hidden="true"
class="ty-skeleton ty-skeleton_round"
/>
</div>
</DocumentFragment>
`;
27 changes: 18 additions & 9 deletions packages/react/src/skeleton/__tests__/skeleton.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,30 @@ describe('<Skeleton />', () => {

it('should render correctly', () => {
const { container } = render(<Skeleton />);
expect(container.firstChild).toHaveClass('ty-skeleton');
expect(container.firstChild).toHaveAttribute('role', 'status');
expect(container.querySelector('.ty-skeleton')).toBeInTheDocument();
expect(container.querySelector('.ty-skeleton')).toHaveClass('ty-skeleton_round');
});

it('should render with active animation', () => {
const { container } = render(<Skeleton active />);
expect(container.firstChild).toHaveClass('ty-skeleton_active');
it('should render with shimmer animation', () => {
const { container } = render(<Skeleton animation="shimmer" />);
expect(container.querySelector('.ty-skeleton')).toHaveClass('ty-skeleton_animated_shimmer');
});

it('should render rounded', () => {
const { container } = render(<Skeleton rounded />);
expect(container.firstChild).toHaveClass('ty-skeleton_rounded');
it('should render shaped block', () => {
const { container } = render(<Skeleton shape="circle" width={40} height={40} />);
expect(container.querySelector('.ty-skeleton')).toHaveClass('ty-skeleton_circle');
});

it('should render children', () => {
const { getByText } = render(<Skeleton>Loading content</Skeleton>);
it('should render children when loading is false', () => {
const { getByText, queryByRole } = render(<Skeleton loading={false}>Loading content</Skeleton>);
expect(getByText('Loading content')).toBeInTheDocument();
expect(queryByRole('status')).not.toBeInTheDocument();
});

it('should render structured skeleton', () => {
const { container } = render(<Skeleton avatar title paragraph={{ rows: 2 }} />);
expect(container.querySelector('.ty-skeleton__group')).toBeInTheDocument();
expect(container.querySelectorAll('.ty-skeleton__text-row')).toHaveLength(3);
});
});
27 changes: 19 additions & 8 deletions packages/react/src/skeleton/demo/Active.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
import React from 'react';
import { Skeleton } from '@tiny-design/react';
import { Flex, Radio, Skeleton } from '@tiny-design/react';

type AnimationMode = 'shimmer' | 'pulse';

export default function ActiveDemo() {
const [animation, setAnimation] = React.useState<AnimationMode>('shimmer');

return (
<>
<Skeleton active style={{ width: 300 }} />
<Skeleton active />
<Skeleton active />
<Skeleton active />
</>
<Flex vertical gap={16}>
<Radio.Group value={animation} onChange={(value) => setAnimation(value as AnimationMode)}>
<Radio value="shimmer">shimmer</Radio>
<Radio value="pulse">pulse</Radio>
</Radio.Group>

<div>
<Skeleton animation={animation} width={300} />
<Skeleton animation={animation} />
<Skeleton animation={animation} />
<Skeleton animation={animation} />
</div>
</Flex>
);
}
}
4 changes: 2 additions & 2 deletions packages/react/src/skeleton/demo/Basic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import { Skeleton } from '@tiny-design/react';
export default function BasicDemo() {
return (
<>
<Skeleton style={{ width: 300 }} />
<Skeleton width={300} />
<Skeleton />
<Skeleton />
<Skeleton />
</>
);
}
}
29 changes: 12 additions & 17 deletions packages/react/src/skeleton/demo/Combination.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,20 @@
import React from 'react';
import { Skeleton, ConfigProvider, Row, Col } from '@tiny-design/react';
import { Skeleton, ConfigProvider, Flex } from '@tiny-design/react';

export default function CombinationDemo() {
return (
<ConfigProvider shimmer>
<Row>
<Col span={2}>
<Skeleton rounded style={{ width: 50, height: 50 }} />
</Col>
<Col span={22}>
<ConfigProvider skeleton={{ animation: 'shimmer' }}>
<Flex gap="sm" vertical>
<Flex gap="sm">
<Skeleton.Avatar size={50} />
<div>
<Skeleton style={{ width: 300 }} />
<Skeleton width={300} />
<Skeleton width={300} />
</div>
<div>
<Skeleton style={{ width: 300 }} />
</div>
</Col>
</Row>
<Skeleton />
<Skeleton />
<Skeleton />
</Flex>
<Skeleton title paragraph={{ rows: 2 }} />
<Skeleton title paragraph={{ rows: 4 }} />
</Flex>
</ConfigProvider>
);
}
}
84 changes: 84 additions & 0 deletions packages/react/src/skeleton/demo/Composed.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import React from 'react';
import { Flex, Skeleton, useTheme } from '@tiny-design/react';

export default function ComposedDemo() {
const { resolvedTheme } = useTheme();
const isDark = resolvedTheme === 'dark';

const shellStyle: React.CSSProperties = {
padding: 18,
border: `1px solid ${isDark ? '#303030' : '#e2e8f0'}`,
borderRadius: 20,
background: isDark
? 'linear-gradient(180deg, #1f1f1f 0%, #262626 100%)'
: 'linear-gradient(180deg, #ffffff 0%, #f8fafc 100%)',
boxShadow: isDark ? 'none' : '0 10px 30px rgba(15, 23, 42, 0.04)',
};

const sectionLabelStyle: React.CSSProperties = {
marginBottom: 14,
fontSize: 11,
fontWeight: 700,
letterSpacing: '0.08em',
textTransform: 'uppercase',
color: isDark ? '#8f8f8f' : '#64748b',
};

const statCardStyle: React.CSSProperties = {
padding: 12,
border: `1px solid ${isDark ? '#3a3a3a' : '#e2e8f0'}`,
borderRadius: 16,
background: isDark ? '#242424' : 'rgba(255, 255, 255, 0.75)',
};

return (
<div style={shellStyle}>
<div style={sectionLabelStyle}>Profile Summary</div>
<Flex align="flex-start" gap={16}>
<Skeleton.Avatar size={64} animation="shimmer" />
<Flex vertical gap={14} style={{ flex: 1 }}>
<Flex justify="space-between" align="center" gap={12}>
<Skeleton.Text rows={1} width="42%" animation="shimmer" />
<Skeleton.Block shape="round" width={74} height={24} animation="pulse" />
</Flex>
<Skeleton.Text rows={2} widths={['96%', '58%']} animation="shimmer" />

<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(3, minmax(0, 1fr))',
gap: 12,
}}
>
<div style={statCardStyle}>
<Skeleton.Text rows={1} width="52%" animation="pulse" />
<div style={{ marginTop: 12 }}>
<Skeleton.Block shape="round" width="72%" height={28} animation="shimmer" />
</div>
</div>
<div style={statCardStyle}>
<Skeleton.Text rows={1} width="48%" animation="pulse" />
<div style={{ marginTop: 12 }}>
<Skeleton.Block shape="round" width="68%" height={28} animation="shimmer" />
</div>
</div>
<div style={statCardStyle}>
<Skeleton.Text rows={1} width="44%" animation="pulse" />
<div style={{ marginTop: 12 }}>
<Skeleton.Block shape="round" width="66%" height={28} animation="shimmer" />
</div>
</div>
</div>

<Flex justify="space-between" align="center" gap={12}>
<Skeleton.Text rows={1} width="32%" animation="pulse" />
<Flex gap={10}>
<Skeleton.Block shape="round" width={88} height={34} animation="pulse" />
<Skeleton.Block shape="round" width={112} height={34} animation="shimmer" />
</Flex>
</Flex>
</Flex>
</Flex>
</div>
);
}
49 changes: 49 additions & 0 deletions packages/react/src/skeleton/demo/Loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React from 'react';
import { Button, Card, Flex, Skeleton, Tag } from '@tiny-design/react';

export default function LoadingDemo() {
const [loading, setLoading] = React.useState(true);

return (
<Flex vertical gap={12}>
<Button size="sm" onClick={() => setLoading((prev) => !prev)} style={{ alignSelf: 'flex-start' }}>
{loading ? 'Show content' : 'Show skeleton'}
</Button>
<Skeleton
loading={loading}
animation="shimmer"
avatar={{ size: 'lg' }}
title={{ width: '32%' }}
paragraph={{ rows: 3, widths: ['100%', '88%', '56%'] }}
>
<Card style={{ maxWidth: 520 }}>
<Card.Content>
<Flex vertical gap={12}>
<Flex justify="space-between" align="center">
<Flex align="center" gap={12}>
<div
style={{
width: 48,
height: 48,
borderRadius: '50%',
background: 'linear-gradient(135deg, #0f766e, #14b8a6)',
}}
/>
<div>
<div style={{ fontWeight: 600 }}>API Sync Health</div>
<div style={{ fontSize: 12, color: '#64748b' }}>Updated 2 minutes ago</div>
</div>
</Flex>
<Tag color="success">Healthy</Tag>
</Flex>
<div style={{ color: '#475569', lineHeight: 1.6 }}>
14 upstream services are responding within the expected latency band. No manual action is
required for the current release window.
</div>
</Flex>
</Card.Content>
</Card>
</Skeleton>
</Flex>
);
}
Loading
Loading