Skip to content

[email protected] #73

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 16, 2025
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
5 changes: 5 additions & 0 deletions .changeset/happy-glasses-dance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"notion-to-jsx": patch
---

Update link property type and access nested url property for text links, add loading skeleton to image and cover components
2 changes: 1 addition & 1 deletion apps/renderer-storybook/scripts/fetchNotionProperties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const __dirname = dirname(__filename);
dotenv.config({ path: resolve(__dirname, '../.env.local') });

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

// ? using this script : pnpx tsx scripts/fetchNotionProperties.ts
async function fetchAndSaveProperties() {
Expand Down
10 changes: 5 additions & 5 deletions apps/renderer-storybook/src/sample-data/notionProperties.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"Date": {
"start": "2025-01-18",
"start": "2024-11-09",
"end": null,
"time_zone": null
},
Expand All @@ -9,10 +9,10 @@
"name": "WEB",
"color": "default"
},
"Slug": "notion-lib-1",
"Slug": "changeset-github-action",
"isPublished": true,
"Desc": "노션 페이지에 글을 쓰면 별도 배포 없이 포스팅되는 라이브러리를 직접 만들기로 했다!",
"Desc": "changeset과 github action을 도입해 버전 관리하는 방법을 공유한다!",
"Tags": null,
"Name": "Notion API로 블로그 만들기 (with npm) - 1",
"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"
"Name": "changeset과 github action으로 버전 관리하기",
"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"
}
25 changes: 23 additions & 2 deletions packages/notion-to-jsx/src/components/Cover/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,33 @@
import { cover } from './styles.css';
import { useState } from 'react';
import { coverContainer, skeletonWrapper, imageStyle } from './styles.css';
import Skeleton from '../Skeleton';

interface CoverProps {
src: string;
alt: string;
}

/**
* 노션 페이지 상단에 표시되는 커버 이미지 컴포넌트
* 이미지 로딩 중에는 스켈레톤 UI를 표시하고, 로딩 완료 시 자연스럽게 이미지로 전환됩니다.
*/
const Cover = ({ src, alt }: CoverProps) => {
return <img src={src} alt={alt} className={cover} />;
const [isLoaded, setIsLoaded] = useState(false);

return (
<div className={coverContainer}>
<div className={skeletonWrapper({ isLoaded })}>
<Skeleton variant="image" />
</div>
<img
src={src}
alt={alt}
className={imageStyle({ isLoaded })}
onLoad={() => setIsLoaded(true)}
loading="lazy"
/>
</div>
);
};

export default Cover;
61 changes: 56 additions & 5 deletions packages/notion-to-jsx/src/components/Cover/styles.css.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,69 @@
import { style } from '@vanilla-extract/css';
import { recipe } from '@vanilla-extract/recipes';

export const cover = style({
export const coverContainer = style({
position: 'relative',
width: '100%',
maxWidth: '56.25rem',
height: '30vh',
display: 'block',
objectFit: 'cover',
objectPosition: 'center 50%',
borderRadius: '1.5rem',
margin: '0 auto',
borderRadius: '1.5rem',
overflow: 'hidden',
boxShadow: '2px 2px 8px 4px hsla(0,0%,6%,.1)',
'@media': {
'(max-width: 900px)': {
borderRadius: '0.5rem',
height: '25vh',
},
},
});

export const skeletonWrapper = recipe({
base: {
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
zIndex: 1,
transition: 'opacity 0.3s ease',
},
variants: {
isLoaded: {
true: {
opacity: 0,
},
false: {
opacity: 1,
},
},
},
defaultVariants: {
isLoaded: false,
},
});

export const imageStyle = recipe({
base: {
width: '100%',
height: '100%',
objectFit: 'cover',
objectPosition: 'center 50%',
display: 'block',
zIndex: 2,
transition: 'opacity 0.3s ease',
},
variants: {
isLoaded: {
true: {
opacity: 1,
},
false: {
opacity: 0,
},
},
},
defaultVariants: {
isLoaded: false,
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import { MemoizedRichText } from '../MemoizedComponents';
import {
imageContainer,
imageWrapper,
styledImage,
placeholder,
imageStyle,
caption,
skeletonWrapper,
} from './styles.css';
import { RichTextItem } from '../RichText/RichTexts';
import Skeleton from '../../../Skeleton';

export interface ImageFormat {
block_width?: number;
Expand All @@ -26,38 +27,6 @@ export interface ImageProps {

const MAX_WIDTH = 720;

// 이미지 스타일 유틸리티 함수
const getImageStyles = (format?: ImageFormat, isColumn: boolean = false) => {
// width 계산 로직
const getWidthStyle = () => {
if (
!isColumn &&
format?.block_aspect_ratio &&
format.block_aspect_ratio < 1
) {
return `${format.block_aspect_ratio * 100}%`;
}

if (format?.block_width) {
return format.block_width > MAX_WIDTH
? '100%'
: `${format.block_width}px`;
}

return '100%';
};

// aspectRatio 계산 로직
const getAspectRatioStyle = () => {
return format?.block_aspect_ratio ? `${format.block_aspect_ratio}` : 'auto';
};

return {
width: getWidthStyle(),
aspectRatio: getAspectRatioStyle(),
};
};

// 이미지 태그에 사용되는 aspectRatio 스타일
const getImageTagStyle = (format?: ImageFormat) => {
return format?.block_aspect_ratio
Expand All @@ -76,41 +45,12 @@ const Image: React.FC<ImageProps> = ({

return (
<div className={imageContainer}>
<div
className={imageWrapper({
hasWidth: !!format?.block_width,
})}
style={getImageStyles(format, isColumn)}
>
{!isLoaded && (
<div className={placeholder} style={getImageStyles(format, isColumn)}>
<svg
width="38"
height="38"
viewBox="0 0 38 38"
xmlns="http://www.w3.org/2000/svg"
stroke="#888"
>
<g fill="none" fillRule="evenodd">
<g transform="translate(1 1)" strokeWidth="2">
<circle strokeOpacity=".5" cx="18" cy="18" r="18" />
<path d="M36 18c0-9.94-8.06-18-18-18">
<animateTransform
attributeName="transform"
type="rotate"
from="0 18 18"
to="360 18 18"
dur="1s"
repeatCount="indefinite"
/>
</path>
</g>
</g>
</svg>
</div>
)}
<div className={imageWrapper} style={getImageStyles(format, isColumn)}>
<div className={skeletonWrapper({ isLoaded })}>
<Skeleton variant="image" />
</div>
<img
className={styledImage({
className={imageStyle({
loaded: isLoaded,
hasAspectRatio: !!format?.block_aspect_ratio,
})}
Expand All @@ -133,3 +73,35 @@ const Image: React.FC<ImageProps> = ({
};

export default Image;

// 이미지 스타일 유틸리티 함수
const getImageStyles = (format?: ImageFormat, isColumn: boolean = false) => {
// width 계산 로직
const getWidthStyle = () => {
if (
!isColumn &&
format?.block_aspect_ratio &&
format.block_aspect_ratio < 1
) {
return `${format.block_aspect_ratio * 100}%`;
}

if (format?.block_width) {
return format.block_width > MAX_WIDTH
? '100%'
: `${format.block_width}px`;
}

return '100%';
};

// aspectRatio 계산 로직
const getAspectRatioStyle = () => {
return format?.block_aspect_ratio ? `${format.block_aspect_ratio}` : 'auto';
};

return {
width: getWidthStyle(),
aspectRatio: getAspectRatioStyle(),
};
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { style } from '@vanilla-extract/css';
import { recipe } from '@vanilla-extract/recipes';
import { createVar, fallbackVar } from '@vanilla-extract/css';
import { vars } from '../../../../styles/theme.css';

export const imageContainer = style({
Expand All @@ -15,36 +14,18 @@ export const imageContainer = style({
alignItems: 'center',
});

export const imageWidthVar = createVar();
export const imageAspectRatioVar = createVar();

export const imageWrapper = recipe({
base: {
position: 'relative',
maxWidth: '100%',
width: fallbackVar(imageWidthVar, '100%'),
},
variants: {
hasWidth: {
true: {},
false: {
width: '100%',
},
},
},
defaultVariants: {
hasWidth: false,
},
export const imageWrapper = style({
position: 'relative',
maxWidth: '100%',
});

export const styledImage = recipe({
export const imageStyle = recipe({
base: {
width: '100%',
height: 'auto',
display: 'block',
transition: 'opacity 0.3s ease',
objectFit: 'contain',
aspectRatio: fallbackVar(imageAspectRatioVar, 'auto'),
},
variants: {
loaded: {
Expand All @@ -53,12 +34,16 @@ export const styledImage = recipe({
},
false: {
opacity: 0,
height: 0,
},
},
hasAspectRatio: {
true: {},
false: {},
true: {
// aspectRatio는 recipe 단계에서는 빈 객체로 두고
// 컴포넌트에서 동적으로 계산된 값으로 채워집니다
},
false: {
aspectRatio: 'auto',
},
},
},
defaultVariants: {
Expand All @@ -67,15 +52,34 @@ export const styledImage = recipe({
},
});

export const placeholder = style({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
});

export const caption = style({
textAlign: 'center',
color: vars.colors.secondary,
marginTop: vars.spacing.sm,
fontSize: vars.typography.fontSize.small,
});

export const skeletonWrapper = recipe({
base: {
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
zIndex: 1,
transition: 'opacity 0.3s ease',
},
variants: {
isLoaded: {
true: {
opacity: 0,
},
false: {
opacity: 1,
},
},
},
defaultVariants: {
isLoaded: false,
},
});
Loading