Skip to content

Commit 9c824c0

Browse files
authored
Merge pull request #73 from 01-binary/[email protected]
[email protected]
2 parents 8b7f6f1 + 3493118 commit 9c824c0

File tree

10 files changed

+279
-116
lines changed

10 files changed

+279
-116
lines changed

.changeset/happy-glasses-dance.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"notion-to-jsx": patch
3+
---
4+
5+
Update link property type and access nested url property for text links, add loading skeleton to image and cover components

apps/renderer-storybook/scripts/fetchNotionProperties.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const __dirname = dirname(__filename);
1212
dotenv.config({ path: resolve(__dirname, '../.env.local') });
1313

1414
// 페이지 ID
15-
const PAGE_ID = '1239c6bf-2b17-8076-a838-d17ca1c89783';
15+
const PAGE_ID = '1399c6bf-2b17-80f4-bfcf-e81ca24d2c5d';
1616

1717
// ? using this script : pnpx tsx scripts/fetchNotionProperties.ts
1818
async function fetchAndSaveProperties() {
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"Date": {
3-
"start": "2025-01-18",
3+
"start": "2024-11-09",
44
"end": null,
55
"time_zone": null
66
},
@@ -9,10 +9,10 @@
99
"name": "WEB",
1010
"color": "default"
1111
},
12-
"Slug": "notion-lib-1",
12+
"Slug": "changeset-github-action",
1313
"isPublished": true,
14-
"Desc": "노션 페이지에 글을 쓰면 별도 배포 없이 포스팅되는 라이브러리를 직접 만들기로 했다!",
14+
"Desc": "changeset과 github action을 도입해 버전 관리하는 방법을 공유한다!",
1515
"Tags": null,
16-
"Name": "Notion API로 블로그 만들기 (with npm) - 1",
17-
"coverUrl": "https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2Fcd7314a5-d906-43b0-81e7-42eff82c02a3%2F8423f797-5e58-45ea-a201-25587375c59d%2F1692217660016.png?table=block&id=1239c6bf-2b17-8076-a838-d17ca1c89783&cache=v2"
16+
"Name": "changeset과 github action으로 버전 관리하기",
17+
"coverUrl": "https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2Fcd7314a5-d906-43b0-81e7-42eff82c02a3%2Fce42e11d-7b8b-462c-b1df-9015be63135b%2Fchangesets-banner-light.png?table=block&id=1399c6bf-2b17-80f4-bfcf-e81ca24d2c5d&cache=v2"
1818
}
Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,33 @@
1-
import { cover } from './styles.css';
1+
import { useState } from 'react';
2+
import { coverContainer, skeletonWrapper, imageStyle } from './styles.css';
3+
import Skeleton from '../Skeleton';
24

35
interface CoverProps {
46
src: string;
57
alt: string;
68
}
79

10+
/**
11+
* 노션 페이지 상단에 표시되는 커버 이미지 컴포넌트
12+
* 이미지 로딩 중에는 스켈레톤 UI를 표시하고, 로딩 완료 시 자연스럽게 이미지로 전환됩니다.
13+
*/
814
const Cover = ({ src, alt }: CoverProps) => {
9-
return <img src={src} alt={alt} className={cover} />;
15+
const [isLoaded, setIsLoaded] = useState(false);
16+
17+
return (
18+
<div className={coverContainer}>
19+
<div className={skeletonWrapper({ isLoaded })}>
20+
<Skeleton variant="image" />
21+
</div>
22+
<img
23+
src={src}
24+
alt={alt}
25+
className={imageStyle({ isLoaded })}
26+
onLoad={() => setIsLoaded(true)}
27+
loading="lazy"
28+
/>
29+
</div>
30+
);
1031
};
1132

1233
export default Cover;
Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,69 @@
11
import { style } from '@vanilla-extract/css';
2+
import { recipe } from '@vanilla-extract/recipes';
23

3-
export const cover = style({
4+
export const coverContainer = style({
5+
position: 'relative',
46
width: '100%',
57
maxWidth: '56.25rem',
68
height: '30vh',
7-
display: 'block',
8-
objectFit: 'cover',
9-
objectPosition: 'center 50%',
10-
borderRadius: '1.5rem',
119
margin: '0 auto',
10+
borderRadius: '1.5rem',
11+
overflow: 'hidden',
12+
boxShadow: '2px 2px 8px 4px hsla(0,0%,6%,.1)',
1213
'@media': {
1314
'(max-width: 900px)': {
1415
borderRadius: '0.5rem',
1516
height: '25vh',
1617
},
1718
},
1819
});
20+
21+
export const skeletonWrapper = recipe({
22+
base: {
23+
position: 'absolute',
24+
top: 0,
25+
left: 0,
26+
width: '100%',
27+
height: '100%',
28+
zIndex: 1,
29+
transition: 'opacity 0.3s ease',
30+
},
31+
variants: {
32+
isLoaded: {
33+
true: {
34+
opacity: 0,
35+
},
36+
false: {
37+
opacity: 1,
38+
},
39+
},
40+
},
41+
defaultVariants: {
42+
isLoaded: false,
43+
},
44+
});
45+
46+
export const imageStyle = recipe({
47+
base: {
48+
width: '100%',
49+
height: '100%',
50+
objectFit: 'cover',
51+
objectPosition: 'center 50%',
52+
display: 'block',
53+
zIndex: 2,
54+
transition: 'opacity 0.3s ease',
55+
},
56+
variants: {
57+
isLoaded: {
58+
true: {
59+
opacity: 1,
60+
},
61+
false: {
62+
opacity: 0,
63+
},
64+
},
65+
},
66+
defaultVariants: {
67+
isLoaded: false,
68+
},
69+
});

packages/notion-to-jsx/src/components/Renderer/components/Image/Image.tsx

Lines changed: 40 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ import { MemoizedRichText } from '../MemoizedComponents';
33
import {
44
imageContainer,
55
imageWrapper,
6-
styledImage,
7-
placeholder,
6+
imageStyle,
87
caption,
8+
skeletonWrapper,
99
} from './styles.css';
1010
import { RichTextItem } from '../RichText/RichTexts';
11+
import Skeleton from '../../../Skeleton';
1112

1213
export interface ImageFormat {
1314
block_width?: number;
@@ -26,38 +27,6 @@ export interface ImageProps {
2627

2728
const MAX_WIDTH = 720;
2829

29-
// 이미지 스타일 유틸리티 함수
30-
const getImageStyles = (format?: ImageFormat, isColumn: boolean = false) => {
31-
// width 계산 로직
32-
const getWidthStyle = () => {
33-
if (
34-
!isColumn &&
35-
format?.block_aspect_ratio &&
36-
format.block_aspect_ratio < 1
37-
) {
38-
return `${format.block_aspect_ratio * 100}%`;
39-
}
40-
41-
if (format?.block_width) {
42-
return format.block_width > MAX_WIDTH
43-
? '100%'
44-
: `${format.block_width}px`;
45-
}
46-
47-
return '100%';
48-
};
49-
50-
// aspectRatio 계산 로직
51-
const getAspectRatioStyle = () => {
52-
return format?.block_aspect_ratio ? `${format.block_aspect_ratio}` : 'auto';
53-
};
54-
55-
return {
56-
width: getWidthStyle(),
57-
aspectRatio: getAspectRatioStyle(),
58-
};
59-
};
60-
6130
// 이미지 태그에 사용되는 aspectRatio 스타일
6231
const getImageTagStyle = (format?: ImageFormat) => {
6332
return format?.block_aspect_ratio
@@ -76,41 +45,12 @@ const Image: React.FC<ImageProps> = ({
7645

7746
return (
7847
<div className={imageContainer}>
79-
<div
80-
className={imageWrapper({
81-
hasWidth: !!format?.block_width,
82-
})}
83-
style={getImageStyles(format, isColumn)}
84-
>
85-
{!isLoaded && (
86-
<div className={placeholder} style={getImageStyles(format, isColumn)}>
87-
<svg
88-
width="38"
89-
height="38"
90-
viewBox="0 0 38 38"
91-
xmlns="http://www.w3.org/2000/svg"
92-
stroke="#888"
93-
>
94-
<g fill="none" fillRule="evenodd">
95-
<g transform="translate(1 1)" strokeWidth="2">
96-
<circle strokeOpacity=".5" cx="18" cy="18" r="18" />
97-
<path d="M36 18c0-9.94-8.06-18-18-18">
98-
<animateTransform
99-
attributeName="transform"
100-
type="rotate"
101-
from="0 18 18"
102-
to="360 18 18"
103-
dur="1s"
104-
repeatCount="indefinite"
105-
/>
106-
</path>
107-
</g>
108-
</g>
109-
</svg>
110-
</div>
111-
)}
48+
<div className={imageWrapper} style={getImageStyles(format, isColumn)}>
49+
<div className={skeletonWrapper({ isLoaded })}>
50+
<Skeleton variant="image" />
51+
</div>
11252
<img
113-
className={styledImage({
53+
className={imageStyle({
11454
loaded: isLoaded,
11555
hasAspectRatio: !!format?.block_aspect_ratio,
11656
})}
@@ -133,3 +73,35 @@ const Image: React.FC<ImageProps> = ({
13373
};
13474

13575
export default Image;
76+
77+
// 이미지 스타일 유틸리티 함수
78+
const getImageStyles = (format?: ImageFormat, isColumn: boolean = false) => {
79+
// width 계산 로직
80+
const getWidthStyle = () => {
81+
if (
82+
!isColumn &&
83+
format?.block_aspect_ratio &&
84+
format.block_aspect_ratio < 1
85+
) {
86+
return `${format.block_aspect_ratio * 100}%`;
87+
}
88+
89+
if (format?.block_width) {
90+
return format.block_width > MAX_WIDTH
91+
? '100%'
92+
: `${format.block_width}px`;
93+
}
94+
95+
return '100%';
96+
};
97+
98+
// aspectRatio 계산 로직
99+
const getAspectRatioStyle = () => {
100+
return format?.block_aspect_ratio ? `${format.block_aspect_ratio}` : 'auto';
101+
};
102+
103+
return {
104+
width: getWidthStyle(),
105+
aspectRatio: getAspectRatioStyle(),
106+
};
107+
};
Lines changed: 36 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { style } from '@vanilla-extract/css';
22
import { recipe } from '@vanilla-extract/recipes';
3-
import { createVar, fallbackVar } from '@vanilla-extract/css';
43
import { vars } from '../../../../styles/theme.css';
54

65
export const imageContainer = style({
@@ -15,36 +14,18 @@ export const imageContainer = style({
1514
alignItems: 'center',
1615
});
1716

18-
export const imageWidthVar = createVar();
19-
export const imageAspectRatioVar = createVar();
20-
21-
export const imageWrapper = recipe({
22-
base: {
23-
position: 'relative',
24-
maxWidth: '100%',
25-
width: fallbackVar(imageWidthVar, '100%'),
26-
},
27-
variants: {
28-
hasWidth: {
29-
true: {},
30-
false: {
31-
width: '100%',
32-
},
33-
},
34-
},
35-
defaultVariants: {
36-
hasWidth: false,
37-
},
17+
export const imageWrapper = style({
18+
position: 'relative',
19+
maxWidth: '100%',
3820
});
3921

40-
export const styledImage = recipe({
22+
export const imageStyle = recipe({
4123
base: {
4224
width: '100%',
4325
height: 'auto',
4426
display: 'block',
4527
transition: 'opacity 0.3s ease',
4628
objectFit: 'contain',
47-
aspectRatio: fallbackVar(imageAspectRatioVar, 'auto'),
4829
},
4930
variants: {
5031
loaded: {
@@ -53,12 +34,16 @@ export const styledImage = recipe({
5334
},
5435
false: {
5536
opacity: 0,
56-
height: 0,
5737
},
5838
},
5939
hasAspectRatio: {
60-
true: {},
61-
false: {},
40+
true: {
41+
// aspectRatio는 recipe 단계에서는 빈 객체로 두고
42+
// 컴포넌트에서 동적으로 계산된 값으로 채워집니다
43+
},
44+
false: {
45+
aspectRatio: 'auto',
46+
},
6247
},
6348
},
6449
defaultVariants: {
@@ -67,15 +52,34 @@ export const styledImage = recipe({
6752
},
6853
});
6954

70-
export const placeholder = style({
71-
display: 'flex',
72-
alignItems: 'center',
73-
justifyContent: 'center',
74-
});
75-
7655
export const caption = style({
7756
textAlign: 'center',
7857
color: vars.colors.secondary,
7958
marginTop: vars.spacing.sm,
8059
fontSize: vars.typography.fontSize.small,
8160
});
61+
62+
export const skeletonWrapper = recipe({
63+
base: {
64+
position: 'absolute',
65+
top: 0,
66+
left: 0,
67+
width: '100%',
68+
height: '100%',
69+
zIndex: 1,
70+
transition: 'opacity 0.3s ease',
71+
},
72+
variants: {
73+
isLoaded: {
74+
true: {
75+
opacity: 0,
76+
},
77+
false: {
78+
opacity: 1,
79+
},
80+
},
81+
},
82+
defaultVariants: {
83+
isLoaded: false,
84+
},
85+
});

0 commit comments

Comments
 (0)