-
{repoName}
-
- {loading ? 'Loading...' : `NotionX • ${updatedTimeText}`}
+ ) : (
+ // 기본 링크 프리뷰 렌더링
+
-
+ )}
);
};
diff --git a/packages/notion-to-jsx/src/components/Renderer/components/LinkPreview/styles.css.ts b/packages/notion-to-jsx/src/components/Renderer/components/LinkPreview/styles.css.ts
index 1fa9871..4f88a4f 100644
--- a/packages/notion-to-jsx/src/components/Renderer/components/LinkPreview/styles.css.ts
+++ b/packages/notion-to-jsx/src/components/Renderer/components/LinkPreview/styles.css.ts
@@ -8,16 +8,14 @@ export const link = style({
paddingBottom: vars.spacing.xxs,
});
-export const card = style({
+export const preview = style({
display: 'flex',
border: `1px solid ${vars.colors.border}`,
borderRadius: vars.borderRadius.md,
overflow: 'hidden',
transition: 'box-shadow 0.2s ease',
alignItems: 'center',
- maxHeight: '4rem',
padding: vars.spacing.base,
- paddingLeft: vars.spacing.md,
gap: vars.spacing.md,
':hover': {
boxShadow: vars.shadows.md,
@@ -28,7 +26,6 @@ export const content = style({
display: 'flex',
flex: '1 1 auto',
flexDirection: 'column',
- justifyContent: 'space-between',
overflow: 'hidden',
});
@@ -36,14 +33,14 @@ export const iconContainer = style({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
- maxWidth: '2.5rem',
- height: '100%',
+ width: '2.5rem',
+ height: '2.5rem',
flexShrink: 0,
});
export const icon = style({
- width: '2.5rem',
- height: '2.5rem',
+ width: '100%',
+ height: '100%',
objectFit: 'contain',
borderRadius: vars.borderRadius.sm,
});
@@ -57,10 +54,25 @@ export const title = style({
whiteSpace: 'nowrap',
});
-export const updatedText = style({
+export const description = style({
fontSize: vars.typography.fontSize.xs,
color: vars.colors.secondary,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
});
+
+/**
+ * 타입별 특수 스타일: 각 링크 타입에만 필요한 추가 스타일
+ */
+
+// GitHub 프리뷰에만 필요한 스타일
+export const githubPreview = style({
+ maxHeight: '4rem',
+ paddingLeft: vars.spacing.md,
+});
+
+// GitHub 컨텐츠에만 필요한 스타일
+export const githubContent = style({
+ justifyContent: 'space-between',
+});
diff --git a/packages/notion-to-jsx/src/components/Renderer/components/List/ListBlocksRenderer.tsx b/packages/notion-to-jsx/src/components/Renderer/components/List/ListBlocksRenderer.tsx
index de1253e..4a258b6 100644
--- a/packages/notion-to-jsx/src/components/Renderer/components/List/ListBlocksRenderer.tsx
+++ b/packages/notion-to-jsx/src/components/Renderer/components/List/ListBlocksRenderer.tsx
@@ -15,7 +15,6 @@ const RecursiveListItem: React.FC<{
}> = ({ block, index }) => {
const blockProps = {
tabIndex: 0,
- 'data-block-id': block.id,
};
const blockType = block.type;
diff --git a/packages/notion-to-jsx/src/components/Renderer/components/Quote/Quote.tsx b/packages/notion-to-jsx/src/components/Renderer/components/Quote/Quote.tsx
new file mode 100644
index 0000000..05ba591
--- /dev/null
+++ b/packages/notion-to-jsx/src/components/Renderer/components/Quote/Quote.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import { MemoizedRichText } from '../MemoizedComponents';
+import { container } from './styles.css';
+import { RichTextItem } from '../RichText/RichTexts';
+import { richText } from '../RichText/styles.css';
+
+export interface QuoteProps {
+ richTexts: RichTextItem[];
+ tabIndex?: number;
+}
+
+const Quote: React.FC
= ({ richTexts, tabIndex }) => {
+ return (
+
+
+
+ );
+};
+
+export default Quote;
diff --git a/packages/notion-to-jsx/src/components/Renderer/components/Quote/index.ts b/packages/notion-to-jsx/src/components/Renderer/components/Quote/index.ts
new file mode 100644
index 0000000..ecbadeb
--- /dev/null
+++ b/packages/notion-to-jsx/src/components/Renderer/components/Quote/index.ts
@@ -0,0 +1 @@
+export { default as Quote } from './Quote';
diff --git a/packages/notion-to-jsx/src/components/Renderer/components/Quote/styles.css.ts b/packages/notion-to-jsx/src/components/Renderer/components/Quote/styles.css.ts
new file mode 100644
index 0000000..e509316
--- /dev/null
+++ b/packages/notion-to-jsx/src/components/Renderer/components/Quote/styles.css.ts
@@ -0,0 +1,13 @@
+import { style } from '@vanilla-extract/css';
+import { vars } from '../../../../styles/theme.css';
+
+export const container = style({
+ position: 'relative',
+ margin: `${vars.spacing.xs} 0`,
+ padding: `${vars.spacing.xs} 0 ${vars.spacing.xs} 1rem`,
+ borderLeft: '3px solid #e1e1e1',
+ color: '#37352f',
+ fontSize: vars.typography.fontSize.base,
+ lineHeight: vars.typography.lineHeight.base,
+ fontStyle: 'italic',
+});
diff --git a/packages/notion-to-jsx/src/components/Renderer/components/RichText/RichTexts.tsx b/packages/notion-to-jsx/src/components/Renderer/components/RichText/RichTexts.tsx
index 01b02fc..31de5e8 100644
--- a/packages/notion-to-jsx/src/components/Renderer/components/RichText/RichTexts.tsx
+++ b/packages/notion-to-jsx/src/components/Renderer/components/RichText/RichTexts.tsx
@@ -2,13 +2,7 @@ import React from 'react';
import { richText, link } from './styles.css';
export interface RichTextItem {
- type: 'text';
- text: {
- content: string;
- link: string | null;
- };
- content: string;
- link: string | null;
+ type: 'text' | 'mention' | string;
annotations: {
bold: boolean;
italic: boolean;
@@ -25,31 +19,61 @@ export interface RichTextItem {
color: string;
plain_text: string;
href: string | null;
+
+ text?: {
+ content: string;
+ link: string | null;
+ };
}
export interface RichTextProps {
richTexts: RichTextItem[];
}
+/**
+ * 링크 컴포넌트를 생성하는 함수
+ */
+const renderLink = (href: string, content: React.ReactNode) => (
+
+ {content}
+
+);
+
const RichTexts: React.FC = ({ richTexts }) => {
return (
<>
{richTexts.map((text, index) => {
- const { bold, italic, strikethrough, underline, code, color } =
+ const { bold, italic, strikethrough, underline, code } =
text.annotations;
- const content = text.text.link ? (
-
- {text.text.content}
-
- ) : (
- text.text.content
- );
+ // 컨텐츠 렌더링 로직
+ let content: React.ReactNode;
+
+ // TODO: Refactor
+ switch (text.type) {
+ case 'text': {
+ if (text.text) {
+ const { text: textData } = text;
+ content = textData.link
+ ? renderLink(textData.link, textData.content)
+ : textData.content;
+ } else {
+ content = text.plain_text;
+ }
+ break;
+ }
+
+ case 'mention': {
+ content = text.href
+ ? renderLink(text.href, text.plain_text)
+ : text.plain_text;
+ break;
+ }
+
+ default: {
+ content = text.plain_text;
+ }
+ }
// TODO: NOTION COLOR 적용
// const colorValue =
diff --git a/packages/notion-to-jsx/src/components/Renderer/components/Table/Table.tsx b/packages/notion-to-jsx/src/components/Renderer/components/Table/Table.tsx
new file mode 100644
index 0000000..78f5af0
--- /dev/null
+++ b/packages/notion-to-jsx/src/components/Renderer/components/Table/Table.tsx
@@ -0,0 +1,65 @@
+import React from 'react';
+import { tableContainer, table, headerCell, hasRowHeader } from './styles.css';
+import TableRow from './TableRow';
+import { NotionBlock } from '../../../../types';
+
+interface TableProps {
+ block: NotionBlock;
+ tabIndex?: number;
+}
+
+const Table: React.FC = ({ block, tabIndex = 0 }) => {
+ if (!block.table || !block.children) {
+ return null;
+ }
+
+ const { table_width, has_column_header, has_row_header } = block.table;
+ const rows =
+ block.children?.filter(
+ (child: NotionBlock) => child.type === 'table_row'
+ ) || [];
+
+ return (
+
+
+ {rows.length > 0 && (
+ <>
+ {has_column_header && rows[0] && (
+
+
+
+ )}
+
+ {/* 유효한 row만 매핑하도록 필터링 추가 */}
+ {rows
+ .filter(
+ (row): row is NotionBlock =>
+ row !== undefined && row.type === 'table_row'
+ )
+ .map((row: NotionBlock, rowIndex: number) => {
+ // 열 헤더가 있고 첫 번째 행이면 이미 thead에서 렌더링되었으므로 건너뜁니다
+ if (has_column_header && rowIndex === 0) {
+ return null;
+ }
+
+ const actualRowIndex = has_column_header
+ ? rowIndex - 1
+ : rowIndex;
+ // 타입 체크를 통해 row가 실제 Block 타입임을 확인합니다
+ return (
+
+ );
+ })}
+
+ >
+ )}
+
+
+ );
+};
+
+export default Table;
diff --git a/packages/notion-to-jsx/src/components/Renderer/components/Table/TableRow.tsx b/packages/notion-to-jsx/src/components/Renderer/components/Table/TableRow.tsx
new file mode 100644
index 0000000..d5ecae2
--- /dev/null
+++ b/packages/notion-to-jsx/src/components/Renderer/components/Table/TableRow.tsx
@@ -0,0 +1,50 @@
+import React from 'react';
+import { tableCell, firstCell, lastCell, hasRowHeader } from './styles.css';
+import { MemoizedRichText } from '../MemoizedComponents';
+import { NotionBlock } from '../../../../types';
+import { RichTextItem } from '../RichText/RichTexts';
+
+interface TableRowProps {
+ rowBlock: NotionBlock;
+ cellClassName?: string;
+ rowHeaderIndex?: number;
+}
+
+const TableRow: React.FC = ({
+ rowBlock,
+ cellClassName = '',
+ rowHeaderIndex = -1,
+}) => {
+ if (!rowBlock.table_row?.cells) {
+ return null;
+ }
+
+ const { cells } = rowBlock.table_row;
+
+ return (
+
+ {cells.map((cell: RichTextItem[], index: number) => {
+ const isFirstCell = index === 0;
+ const isLastCell = index === cells.length - 1;
+ const isRowHeader = index === rowHeaderIndex;
+
+ let cellClasses = [tableCell, cellClassName];
+
+ if (isFirstCell) cellClasses.push(firstCell);
+ if (isLastCell) cellClasses.push(lastCell);
+ if (isRowHeader) cellClasses.push(hasRowHeader);
+
+ return (
+
+
+ |
+ );
+ })}
+
+ );
+};
+
+export default TableRow;
diff --git a/packages/notion-to-jsx/src/components/Renderer/components/Table/index.ts b/packages/notion-to-jsx/src/components/Renderer/components/Table/index.ts
new file mode 100644
index 0000000..7d14179
--- /dev/null
+++ b/packages/notion-to-jsx/src/components/Renderer/components/Table/index.ts
@@ -0,0 +1,5 @@
+import Table from './Table';
+import TableRow from './TableRow';
+
+export { TableRow };
+export default Table;
diff --git a/packages/notion-to-jsx/src/components/Renderer/components/Table/styles.css.ts b/packages/notion-to-jsx/src/components/Renderer/components/Table/styles.css.ts
new file mode 100644
index 0000000..78d9c38
--- /dev/null
+++ b/packages/notion-to-jsx/src/components/Renderer/components/Table/styles.css.ts
@@ -0,0 +1,48 @@
+import { style } from '@vanilla-extract/css';
+import { vars } from '../../../../styles/theme.css';
+
+export const tableContainer = style({
+ width: '100%',
+ marginTop: vars.spacing.xs,
+ marginBottom: vars.spacing.xs,
+ borderRadius: vars.borderRadius.sm,
+ overflow: 'hidden',
+});
+
+export const table = style({
+ width: '100%',
+ borderCollapse: 'collapse',
+ borderSpacing: 0,
+ fontSize: vars.typography.fontSize.small,
+ color: 'inherit',
+});
+
+export const headerCell = style({
+ backgroundColor: '#f7f6f3',
+ fontWeight: vars.typography.fontWeight.semibold,
+});
+
+export const tableCell = style({
+ position: 'relative',
+ padding: `${vars.spacing.xs} ${vars.spacing.sm}`,
+ minHeight: '2rem',
+ border: '1px solid rgba(55, 53, 47, 0.09)',
+ borderLeft: 'none',
+ borderRight: 'none',
+ verticalAlign: 'top',
+ textAlign: 'left',
+ userSelect: 'text',
+});
+
+export const firstCell = style({
+ borderLeft: '1px solid rgba(55, 53, 47, 0.09)',
+});
+
+export const lastCell = style({
+ borderRight: '1px solid rgba(55, 53, 47, 0.09)',
+});
+
+export const hasRowHeader = style({
+ backgroundColor: '#f7f6f3',
+ fontWeight: vars.typography.fontWeight.semibold,
+});
diff --git a/packages/notion-to-jsx/src/components/Renderer/components/Toggle/Toggle.tsx b/packages/notion-to-jsx/src/components/Renderer/components/Toggle/Toggle.tsx
new file mode 100644
index 0000000..9cfe515
--- /dev/null
+++ b/packages/notion-to-jsx/src/components/Renderer/components/Toggle/Toggle.tsx
@@ -0,0 +1,70 @@
+import React, { useState } from 'react';
+import { NotionBlock } from '../../../../types';
+import {
+ toggleContainer,
+ toggleHeader,
+ toggleIcon,
+ toggleIconOpen,
+ toggleContent
+} from './styles.css';
+import { RichTexts } from '../../components/RichText';
+import BlockRenderer from '../../components/Block/BlockRenderer';
+
+interface ToggleProps {
+ block: NotionBlock;
+ tabIndex?: number;
+ onFocus?: () => void;
+}
+
+const Toggle: React.FC = ({ block, tabIndex = 0, onFocus }) => {
+ const [isOpen, setIsOpen] = useState(false);
+
+ // Toggle이 없거나 children이 없는 경우 렌더링하지 않음
+ if (!block.toggle || !block.children) {
+ return null;
+ }
+
+ const handleToggle = () => {
+ setIsOpen(!isOpen);
+ };
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ handleToggle();
+ }
+ };
+
+ return (
+
+
+
+ ▶
+
+
+
+
+ {isOpen && block.children && (
+
+ {block.children.map((childBlock, index) => (
+
+ ))}
+
+ )}
+
+ );
+};
+
+export default Toggle;
diff --git a/packages/notion-to-jsx/src/components/Renderer/components/Toggle/index.ts b/packages/notion-to-jsx/src/components/Renderer/components/Toggle/index.ts
new file mode 100644
index 0000000..8a9212c
--- /dev/null
+++ b/packages/notion-to-jsx/src/components/Renderer/components/Toggle/index.ts
@@ -0,0 +1,3 @@
+import Toggle from './Toggle';
+
+export { Toggle };
diff --git a/packages/notion-to-jsx/src/components/Renderer/components/Toggle/styles.css.ts b/packages/notion-to-jsx/src/components/Renderer/components/Toggle/styles.css.ts
new file mode 100644
index 0000000..527bfd3
--- /dev/null
+++ b/packages/notion-to-jsx/src/components/Renderer/components/Toggle/styles.css.ts
@@ -0,0 +1,40 @@
+import { style } from '@vanilla-extract/css';
+import { vars } from '../../../../styles/theme.css';
+
+export const toggleContainer = style({
+ position: 'relative',
+});
+
+export const toggleHeader = style({
+ display: 'flex',
+ alignItems: 'center',
+ cursor: 'pointer',
+ fontSize: vars.typography.fontSize.base,
+ fontWeight: vars.typography.fontWeight.normal,
+ color: 'inherit',
+ padding: `${vars.spacing.xs} 0`,
+ borderRadius: vars.borderRadius.sm,
+ ':hover': {
+ background: 'rgba(55, 53, 47, 0.08)',
+ },
+});
+
+export const toggleIcon = style({
+ marginRight: vars.spacing.sm,
+ display: 'inline-flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ transition: 'transform 0.2s ease',
+ width: '1.2rem',
+ height: '1.2rem',
+});
+
+export const toggleIconOpen = style({
+ transform: 'rotate(90deg)',
+});
+
+export const toggleContent = style({
+ paddingLeft: vars.spacing.lg,
+ marginTop: vars.spacing.xs,
+ overflow: 'hidden',
+});
diff --git a/packages/notion-to-jsx/src/types/index.ts b/packages/notion-to-jsx/src/types/index.ts
index 2ead73c..da22fe2 100644
--- a/packages/notion-to-jsx/src/types/index.ts
+++ b/packages/notion-to-jsx/src/types/index.ts
@@ -12,7 +12,11 @@ export interface NotionBlock {
| 'numbered_list_item'
| 'code'
| 'image'
- | 'bookmark';
+ | 'bookmark'
+ | 'table'
+ | 'table_row'
+ | 'quote'
+ | 'toggle';
paragraph?: {
rich_text: RichTextItem[];
color: string;
@@ -52,4 +56,26 @@ export interface NotionBlock {
url: string;
caption: RichTextItem[];
};
+ table?: {
+ table_width: number;
+ has_column_header: boolean;
+ has_row_header: boolean;
+ };
+ table_row?: {
+ cells: RichTextItem[][];
+ };
+ quote?: {
+ rich_text: RichTextItem[];
+ color: string;
+ };
+ toggle?: {
+ rich_text: RichTextItem[];
+ color: string;
+ };
+ children?: NotionBlock[];
+ has_children?: boolean;
+ parent?: {
+ type: string;
+ [key: string]: any;
+ };
}