Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
7 changes: 7 additions & 0 deletions .changeset/shiny-skeleton-loading.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@vapor-ui/core': minor
---

Add Skeleton component

You can see more details about the Skeleton component in the [Skeleton documentation](https://vapor-ui.goorm.io/docs/components/skeleton).
82 changes: 82 additions & 0 deletions apps/website/content/docs/components/skeleton.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
---
title: 'Skeleton'
site_name: 'Skeleton - Vapor Core'
description: '콘텐츠가 로딩 중임을 나타내는 플레이스홀더 요소입니다.'
---

<Demo name="skeleton/default-skeleton">
```json doc-gen:file
{
"file": "./src/components/demo/examples/skeleton/default-skeleton.tsx",
"codeblock": true
}
```
</Demo>

## Property

---

### Shape

Skeleton의 모서리 둥글기를 설정합니다.

<Demo name="skeleton/skeleton-shape">
```json doc-gen:file
{
"file": "./src/components/demo/examples/skeleton/skeleton-shape.tsx",
"codeblock": true
}
```
</Demo>

### Animation

Skeleton의 애니메이션 스타일을 설정합니다.

<Demo name="skeleton/skeleton-animation">
```json doc-gen:file
{
"file": "./src/components/demo/examples/skeleton/skeleton-animation.tsx",
"codeblock": true
}
```
</Demo>

### Size

Skeleton의 높이를 설정합니다.

<Demo name="skeleton/skeleton-size">
```json doc-gen:file
{
"file": "./src/components/demo/examples/skeleton/skeleton-size.tsx",
"codeblock": true
}
```
</Demo>

## Examples

---

### Profile Card

여러 Skeleton 요소를 조합하여 프로필 카드 플레이스홀더를 구성하는 예제입니다.

<Demo name="skeleton/skeleton-composition">
```json doc-gen:file
{
"file": "./src/components/demo/examples/skeleton/skeleton-composition.tsx",
"codeblock": true
}
```
</Demo>

## Props Table

---

### Skeleton

<ComponentPropsTable componentName="skeleton" />
35 changes: 35 additions & 0 deletions apps/website/public/components/generated/skeleton.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"name": "Skeleton",
"displayName": "Skeleton",
"description": "콘텐츠가 로딩 중임을 나타내는 placeholder 요소입니다. `<div>` 요소를 렌더링합니다.",
"props": [
{
"name": "animation",
"type": ["none", "shimmer", "pulse"],
"required": false,
"description": "skeleton의 애니메이션 스타일을 제어합니다.\n\n- `shimmer`: 빛이 스치는 효과\n- `pulse`: 깜빡이는 효과\n- `none`: 애니메이션 비활성화",
"defaultValue": "shimmer"
},
{
"name": "shape",
"type": ["square", "rounded"],
"required": false,
"description": "skeleton의 테두리 반경을 제어합니다.",
"defaultValue": "rounded"
},
{
"name": "size",
"type": ["sm", "md", "lg", "xl"],
"required": false,
"description": "skeleton의 높이를 제어합니다.",
"defaultValue": "md"
},
{
"name": "render",
"type": ["ReactElement", "(props: HTMLProps) => ReactElement"],
"required": false,
"description": "component의 HTML element를 다른 태그로 교체하거나 다른 component와 조합할 수 있습니다.\n\nReactElement 또는 렌더링할 요소를 반환하는 함수를 받습니다."
}
],
"defaultElement": "div"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Skeleton } from '@vapor-ui/core';

export default function DefaultSkeleton() {
return <Skeleton width="200px" />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { HStack, Skeleton, Text, VStack } from '@vapor-ui/core';

export default function SkeletonAnimation() {
return (
<VStack gap="$150">
<HStack gap="$200" alignItems="center">
<Text width="80px" typography="body3" foreground="hint-100">
shimmer
</Text>
<Skeleton animation="shimmer" />
</HStack>
<HStack gap="$200" alignItems="center">
<Text width="80px" typography="body3" foreground="hint-100">
pulse
</Text>
<Skeleton animation="pulse" />
</HStack>
<HStack gap="$200" alignItems="center">
<Text width="80px" typography="body3" foreground="hint-100">
none
</Text>
<Skeleton animation="none" />
</HStack>
</VStack>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Box, HStack, Skeleton, VStack } from '@vapor-ui/core';

export default function SkeletonComposition() {
return (
<Box
padding="$200"
border="1px solid"
borderColor="$normal"
borderRadius="$100"
maxWidth="320px"
>
<HStack gap="$150" alignItems="center">
<Skeleton shape="rounded" width="40px" height="40px" />
<VStack gap="$075">
<Skeleton shape="rounded" size="sm" width="120px" />
<Skeleton shape="rounded" size="sm" width="80px" />
</VStack>
</HStack>
<VStack gap="$075" marginTop="$200">
<Skeleton shape="square" size="sm" />
<Skeleton shape="square" size="sm" width="90%" />
<Skeleton shape="square" size="sm" width="75%" />
</VStack>
</Box>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { HStack, Skeleton, Text, VStack } from '@vapor-ui/core';

export default function SkeletonShape() {
return (
<VStack gap="$150">
<HStack gap="$200" alignItems="center">
<Text width="80px" typography="body3" foreground="hint-100">
rounded
</Text>
<Skeleton shape="rounded" width="200px" />
</HStack>
<HStack gap="$200" alignItems="center">
<Text width="80px" typography="body3" foreground="hint-100">
square
</Text>
<Skeleton shape="square" width="200px" />
</HStack>
</VStack>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { HStack, Skeleton, Text, VStack } from '@vapor-ui/core';

export default function SkeletonSize() {
return (
<VStack gap="$150">
<HStack gap="$200" alignItems="center">
<Text width="40px" typography="body3" foreground="hint-100">
sm
</Text>
<Skeleton size="sm" />
</HStack>
<HStack gap="$200" alignItems="center">
<Text width="40px" typography="body3" foreground="hint-100">
md
</Text>
<Skeleton size="md" />
</HStack>
<HStack gap="$200" alignItems="center">
<Text width="40px" typography="body3" foreground="hint-100">
lg
</Text>
<Skeleton size="lg" />
</HStack>
<HStack gap="$200" alignItems="center">
<Text width="40px" typography="body3" foreground="hint-100">
xl
</Text>
<Skeleton size="xl" />
</HStack>
</VStack>
);
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions packages/core/src/components/skeleton/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './skeleton';
79 changes: 79 additions & 0 deletions packages/core/src/components/skeleton/skeleton.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { keyframes } from '@vanilla-extract/css';
import type { RecipeVariants } from '@vanilla-extract/recipes';
import { recipe } from '@vanilla-extract/recipes';

import { layerStyle } from '~/styles/mixins/layer-style.css';
import { vars } from '~/styles/themes.css';

const shimmerKeyframes = keyframes({
to: { backgroundPosition: 'right -6.25rem top 0' },
});

const pulseKeyframes = keyframes({
'0%, 100%': { opacity: 1 },
'50%': { opacity: 0.5 },
});

/**
* Style variants for the Skeleton component.
*/
export const root = recipe({
base: layerStyle('components', {
display: 'block',
overflow: 'hidden',
width: '100%',
backgroundColor: vars.color.gray['100'],

'@media': {
'(prefers-reduced-motion: reduce)': {
animation: 'none',
},
},
}),

defaultVariants: { shape: 'rounded', size: 'md', animation: 'shimmer' },
variants: {
/**
* Controls the border radius of the skeleton.
*/
shape: {
rounded: layerStyle('components', {
borderRadius: '9999px',
}),
square: layerStyle('components', {
borderRadius: vars.size.borderRadius['300'],
}),
},

/**
* Controls the height of the skeleton.
*/
size: {
sm: layerStyle('components', { height: vars.size.dimension['200'] }),
md: layerStyle('components', { height: vars.size.dimension['300'] }),
lg: layerStyle('components', { height: vars.size.dimension['400'] }),
xl: layerStyle('components', { height: vars.size.dimension['500'] }),
},

/**
* Controls the animation style of the skeleton.
*/
animation: {
shimmer: layerStyle('components', {
backgroundImage: `linear-gradient(90deg, ${vars.color.gray['100']}, ${vars.color.gray['050']}, ${vars.color.gray['100']})`,
backgroundPosition: 'left -6.25rem top 0',
backgroundSize: '6.25rem 100%',
backgroundRepeat: 'no-repeat',
animation: `${shimmerKeyframes} 1s ease-in-out infinite`,
}),
pulse: layerStyle('components', {
animation: `${pulseKeyframes} 1.2s ease-in-out infinite`,
}),
none: layerStyle('components', {
animation: 'none',
}),
},
},
});

export type SkeletonVariants = NonNullable<RecipeVariants<typeof root>>;
Loading
Loading