From 98629506acaa9927575de66e9708c6af6bb89b2c Mon Sep 17 00:00:00 2001 From: QH Date: Sat, 27 Jun 2026 22:13:57 +0800 Subject: [PATCH] feat(email-inbox): implement classic inbox view page UI Implement React Native Classic Inbox View with: - New top navigation row with account selector - Navigation buttons: Flow, Dashboard, Classic, Compose - Category buttons: Primary, Promotions, Updates - Updated category icons - Search bar and filter button - Email list UI with clear opened/unopened visual difference - Smooth scroll-based header hide/reveal behavior - Storybook stories for all components Closes #2 --- .../email-inbox-classic-view/package.json | 29 +++ .../src/components/CategoryButtons.tsx | 99 +++++++++++ .../src/components/ClassicInboxView.tsx | 166 ++++++++++++++++++ .../src/components/EmailListItem.tsx | 165 +++++++++++++++++ .../src/components/SearchFilterBar.tsx | 95 ++++++++++ .../src/components/TopNavigation.tsx | 142 +++++++++++++++ .../email-inbox-classic-view/src/index.ts | 5 + .../src/types/index.ts | 60 +++++++ .../storybook/Inbox.stories.tsx | 135 ++++++++++++++ .../email-inbox-classic-view/tsconfig.json | 22 +++ 10 files changed, 918 insertions(+) create mode 100644 packages/email-inbox-classic-view/package.json create mode 100644 packages/email-inbox-classic-view/src/components/CategoryButtons.tsx create mode 100644 packages/email-inbox-classic-view/src/components/ClassicInboxView.tsx create mode 100644 packages/email-inbox-classic-view/src/components/EmailListItem.tsx create mode 100644 packages/email-inbox-classic-view/src/components/SearchFilterBar.tsx create mode 100644 packages/email-inbox-classic-view/src/components/TopNavigation.tsx create mode 100644 packages/email-inbox-classic-view/src/index.ts create mode 100644 packages/email-inbox-classic-view/src/types/index.ts create mode 100644 packages/email-inbox-classic-view/storybook/Inbox.stories.tsx create mode 100644 packages/email-inbox-classic-view/tsconfig.json diff --git a/packages/email-inbox-classic-view/package.json b/packages/email-inbox-classic-view/package.json new file mode 100644 index 00000000..1e7f5cf5 --- /dev/null +++ b/packages/email-inbox-classic-view/package.json @@ -0,0 +1,29 @@ +{ + "name": "@warpspeed/email-inbox-classic-view", + "version": "0.1.0", + "description": "Classic Inbox UI for warpSpeed email - React Native component library", + "private": true, + "scripts": { + "build": "tsc", + "lint": "eslint src/ --ext .ts,.tsx", + "storybook": "storybook dev -p 6006", + "build-storybook": "storybook build" + }, + "dependencies": { + "react": "^18.3.1", + "react-native": "^0.74.3", + "react-native-safe-area-context": "^4.10.1", + "@react-navigation/native": "^6.1.17", + "react-native-reanimated": "^3.14.0", + "@gorhom/bottom-sheet": "^4.6.3" + }, + "devDependencies": { + "@react-native/typescript-config": "^0.74.0", + "@storybook/addon-actions": "^8.1.10", + "@storybook/addon-controls": "^8.1.10", + "@storybook/addon-docs": "^8.1.10", + "@storybook/react-native": "^8.1.10", + "@types/react": "^18.3.3", + "typescript": "^5.5.3" + } +} diff --git a/packages/email-inbox-classic-view/src/components/CategoryButtons.tsx b/packages/email-inbox-classic-view/src/components/CategoryButtons.tsx new file mode 100644 index 00000000..29ca3fdf --- /dev/null +++ b/packages/email-inbox-classic-view/src/components/CategoryButtons.tsx @@ -0,0 +1,99 @@ +import React from 'react'; +import { View, Text, TouchableOpacity, StyleSheet } from 'react-native'; +import { MailLabel, LABEL_ICONS } from '../types'; + +interface CategoryButtonsProps { + activeLabel: MailLabel; + onLabelChange: (label: MailLabel) => void; + counts?: Record; +} + +const LABEL_NAMES: Record = { + primary: 'Primary', + promotions: 'Promotions', + updates: 'Updates', +}; + +/** + * Category buttons for filtering emails by label. + */ +export const CategoryButtons: React.FC = ({ + activeLabel, + onLabelChange, + counts = { primary: 0, promotions: 0, updates: 0 }, +}) => { + const labels: MailLabel[] = ['primary', 'promotions', 'updates']; + + return ( + + {labels.map((label) => ( + onLabelChange(label)} + > + + {label === 'primary' && '⭐'} + {label === 'promotions' && '🏷️'} + {label === 'updates' && '🔄'} + + + {LABEL_NAMES[label]} + + {counts[label] > 0 && ( + + {counts[label]} + + )} + + ))} + + ); +}; + +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + paddingHorizontal: 16, + paddingVertical: 8, + gap: 8, + }, + button: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 8, + paddingHorizontal: 14, + borderRadius: 20, + backgroundColor: '#F2F2F7', + gap: 6, + }, + buttonActive: { + backgroundColor: '#007AFF', + }, + icon: { + fontSize: 14, + }, + label: { + fontSize: 14, + fontWeight: '500', + color: '#3A3A3C', + }, + labelActive: { + color: '#FFFFFF', + }, + badge: { + backgroundColor: '#FF3B30', + borderRadius: 10, + paddingHorizontal: 6, + paddingVertical: 1, + minWidth: 20, + alignItems: 'center', + }, + badgeText: { + color: '#FFFFFF', + fontSize: 11, + fontWeight: '600', + }, +}); + +export default CategoryButtons; diff --git a/packages/email-inbox-classic-view/src/components/ClassicInboxView.tsx b/packages/email-inbox-classic-view/src/components/ClassicInboxView.tsx new file mode 100644 index 00000000..a3d2558c --- /dev/null +++ b/packages/email-inbox-classic-view/src/components/ClassicInboxView.tsx @@ -0,0 +1,166 @@ +import React, { useCallback, useRef, useMemo } from 'react'; +import { + View, + FlatList, + StyleSheet, + Animated as RNAnimated, + NativeSyntheticEvent, + NativeScrollEvent, +} from 'react-native'; +import { TopNavigation } from './TopNavigation'; +import { CategoryButtons } from './CategoryButtons'; +import { SearchFilterBar } from './SearchFilterBar'; +import { EmailListItem } from './EmailListItem'; +import { Email, MailLabel, InboxCategory } from '../types'; + +interface ClassicInboxViewProps { + emails: Email[]; + activeCategory: InboxCategory; + activeLabel: MailLabel; + searchQuery: string; + isRefreshing: boolean; + onNavChange: (navId: string) => void; + onLabelChange: (label: MailLabel) => void; + onSearchChange: (query: string) => void; + onFilterPress: () => void; + onEmailPress: (email: Email) => void; + onRefresh: () => void; +} + +const HEADER_VISIBLE_HEIGHT = 140; // height of header content when shown +const HEADER_HIDDEN_HEIGHT = 0; + +/** + * Classic Inbox View - main page component. + * Features smooth scroll-based header hide/reveal behavior. + */ +export const ClassicInboxView: React.FC = ({ + emails, + activeCategory, + activeLabel, + searchQuery, + isRefreshing, + onNavChange, + onLabelChange, + onSearchChange, + onFilterPress, + onEmailPress, + onRefresh, +}) => { + const scrollY = useRef(new RNAnimated.Value(0)).current; + const lastScrollY = useRef(0); + const isHeaderVisible = useRef(true); + + const headerTranslate = scrollY.interpolate({ + inputRange: [0, 100], + outputRange: [0, -HEADER_VISIBLE_HEIGHT], + extrapolate: 'clamp', + }); + + const handleScroll = useCallback( + RNAnimated.event( + [{ nativeEvent: { contentOffset: { y: scrollY } } }], + { + useNativeDriver: true, + listener: (event: NativeSyntheticEvent) => { + const currentY = event.nativeEvent.contentOffset.y; + const diff = currentY - lastScrollY.current; + + // Determine direction + if (diff > 5 && currentY > 30 && isHeaderVisible.current) { + // Scrolling down - hide header + isHeaderVisible.current = false; + } else if (diff < -5 && !isHeaderVisible.current) { + // Scrolling up - show header + isHeaderVisible.current = true; + } + + lastScrollY.current = currentY; + }, + } + ), + [scrollY] + ); + + const renderEmailItem = useCallback( + ({ item }: { item: Email }) => ( + + ), + [onEmailPress] + ); + + const keyExtractor = useCallback((item: Email) => item.id, []); + + return ( + + {/* Animated Header */} + + + + + + + {/* Email List */} + + + No emails found + + + } + /> + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#FFFFFF', + }, + header: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + backgroundColor: '#FFFFFF', + zIndex: 100, + shadowColor: '#000', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.1, + shadowRadius: 3, + elevation: 3, + }, + listContent: { + marginTop: HEADER_VISIBLE_HEIGHT, + }, + emptyState: { + padding: 40, + alignItems: 'center', + }, + emptyText: { + fontSize: 16, + color: '#8E8E93', + }, +}); + +export default ClassicInboxView; diff --git a/packages/email-inbox-classic-view/src/components/EmailListItem.tsx b/packages/email-inbox-classic-view/src/components/EmailListItem.tsx new file mode 100644 index 00000000..32740967 --- /dev/null +++ b/packages/email-inbox-classic-view/src/components/EmailListItem.tsx @@ -0,0 +1,165 @@ +import React from 'react'; +import { View, Text, TouchableOpacity, StyleSheet } from 'react-native'; +import { Email } from '../types'; + +interface EmailListItemProps { + email: Email; + onPress: (email: Email) => void; +} + +/** + * A single email row in the inbox list. + * Shows clear visual difference between opened and unopened emails. + */ +export const EmailListItem: React.FC = ({ email, onPress }) => { + const timeString = formatTimestamp(email.timestamp); + + return ( + onPress(email)} + activeOpacity={0.7} + > + {/* Sender Avatar */} + + + {email.from.name.charAt(0).toUpperCase()} + + + + {/* Email Content */} + + {/* Header Row: Sender + Time */} + + + {email.from.name} + + + {timeString} + + + + {/* Subject */} + + {email.subject} + + + {/* Preview */} + + {email.preview} + + + + {/* Star */} + {email.isStarred && } + + {/* Unread indicator */} + {!email.isOpened && } + + ); +}; + +function formatTimestamp(date: Date): string { + const now = new Date(); + const diff = now.getTime() - date.getTime(); + const days = Math.floor(diff / (1000 * 60 * 60 * 24)); + + if (days === 0) { + // Today: show time + return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + } else if (days === 1) { + return 'Yesterday'; + } else if (days < 7) { + return date.toLocaleDateString([], { weekday: 'short' }); + } else { + return date.toLocaleDateString([], { month: 'short', day: 'numeric' }); + } +} + +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 12, + paddingHorizontal: 16, + borderBottomWidth: StyleSheet.hairlineWidth, + borderBottomColor: '#E5E5EA', + backgroundColor: '#FFFFFF', + }, + unreadContainer: { + backgroundColor: '#F8F9FF', + }, + avatar: { + width: 40, + height: 40, + borderRadius: 20, + backgroundColor: '#C7C7CC', + justifyContent: 'center', + alignItems: 'center', + marginRight: 12, + }, + unreadAvatar: { + backgroundColor: '#007AFF', + }, + avatarText: { + color: '#FFFFFF', + fontWeight: '600', + fontSize: 16, + }, + content: { + flex: 1, + }, + headerRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 2, + }, + senderName: { + fontSize: 15, + fontWeight: '500', + color: '#1C1C1E', + flex: 1, + marginRight: 8, + }, + unreadText: { + fontWeight: '700', + }, + time: { + fontSize: 12, + color: '#8E8E93', + }, + unreadTime: { + fontWeight: '600', + color: '#007AFF', + }, + subject: { + fontSize: 15, + color: '#1C1C1E', + marginBottom: 2, + }, + preview: { + fontSize: 13, + color: '#8E8E93', + }, + star: { + fontSize: 16, + color: '#FFC107', + marginLeft: 8, + }, + unreadDot: { + width: 8, + height: 8, + borderRadius: 4, + backgroundColor: '#007AFF', + marginLeft: 8, + }, +}); + +export default EmailListItem; diff --git a/packages/email-inbox-classic-view/src/components/SearchFilterBar.tsx b/packages/email-inbox-classic-view/src/components/SearchFilterBar.tsx new file mode 100644 index 00000000..da195f6a --- /dev/null +++ b/packages/email-inbox-classic-view/src/components/SearchFilterBar.tsx @@ -0,0 +1,95 @@ +import React from 'react'; +import { View, TextInput, TouchableOpacity, Text, StyleSheet } from 'react-native'; + +interface SearchFilterBarProps { + searchQuery: string; + onSearchChange: (query: string) => void; + onFilterPress: () => void; +} + +/** + * Search bar and filter button for the inbox. + */ +export const SearchFilterBar: React.FC = ({ + searchQuery, + onSearchChange, + onFilterPress, +}) => { + return ( + + + 🔍 + + {searchQuery.length > 0 && ( + onSearchChange('')} + > + + + )} + + + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 16, + paddingVertical: 8, + gap: 8, + }, + searchContainer: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + backgroundColor: '#F2F2F7', + borderRadius: 10, + paddingHorizontal: 12, + height: 36, + }, + searchIcon: { + fontSize: 14, + marginRight: 6, + }, + searchInput: { + flex: 1, + fontSize: 15, + color: '#1C1C1E', + padding: 0, + }, + clearButton: { + padding: 4, + }, + clearIcon: { + fontSize: 14, + color: '#8E8E93', + }, + filterButton: { + width: 36, + height: 36, + borderRadius: 10, + backgroundColor: '#F2F2F7', + justifyContent: 'center', + alignItems: 'center', + }, + filterIcon: { + fontSize: 18, + color: '#007AFF', + }, +}); + +export default SearchFilterBar; diff --git a/packages/email-inbox-classic-view/src/components/TopNavigation.tsx b/packages/email-inbox-classic-view/src/components/TopNavigation.tsx new file mode 100644 index 00000000..47fc17f8 --- /dev/null +++ b/packages/email-inbox-classic-view/src/components/TopNavigation.tsx @@ -0,0 +1,142 @@ +import React from 'react'; +import { View, Text, TouchableOpacity, StyleSheet } from 'react-native'; +import { NAV_BUTTONS } from '../types'; + +interface TopNavigationProps { + activeNav: string; + onNavChange: (navId: string) => void; + accountName?: string; + onAccountPress?: () => void; +} + +/** + * New top navigation row with account selector and navigation buttons. + */ +export const TopNavigation: React.FC = ({ + activeNav, + onNavChange, + accountName = 'Account', + onAccountPress, +}) => { + return ( + + {/* Account Selector */} + + + {accountName.charAt(0).toUpperCase()} + + + {accountName} + + + + + {/* Navigation Buttons */} + + {NAV_BUTTONS.map((button) => ( + onNavChange(button.id)} + > + + {button.icon === 'flow' && '⚡'} + {button.icon === 'dashboard' && '📊'} + {button.icon === 'classic' && '📧'} + {button.icon === 'compose' && '✏️'} + + + {button.label} + + + ))} + + + ); +}; + +const styles = StyleSheet.create({ + container: { + backgroundColor: '#FFFFFF', + paddingTop: 8, + paddingHorizontal: 16, + paddingBottom: 8, + borderBottomWidth: 1, + borderBottomColor: '#E5E5EA', + }, + accountButton: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: 12, + }, + avatar: { + width: 32, + height: 32, + borderRadius: 16, + backgroundColor: '#007AFF', + justifyContent: 'center', + alignItems: 'center', + marginRight: 8, + }, + avatarText: { + color: '#FFFFFF', + fontWeight: '600', + fontSize: 14, + }, + accountInfo: { + flexDirection: 'row', + alignItems: 'center', + flex: 1, + }, + accountName: { + fontSize: 16, + fontWeight: '600', + color: '#1C1C1E', + marginRight: 4, + }, + chevron: { + fontSize: 12, + color: '#8E8E93', + }, + navRow: { + flexDirection: 'row', + justifyContent: 'space-around', + }, + navButton: { + alignItems: 'center', + paddingVertical: 6, + paddingHorizontal: 12, + borderRadius: 8, + }, + navButtonActive: { + backgroundColor: '#007AFF15', + }, + navIcon: { + fontSize: 20, + marginBottom: 2, + }, + navIconActive: { + opacity: 1, + }, + navLabel: { + fontSize: 11, + color: '#8E8E93', + fontWeight: '500', + }, + navLabelActive: { + color: '#007AFF', + fontWeight: '600', + }, +}); + +export default TopNavigation; diff --git a/packages/email-inbox-classic-view/src/index.ts b/packages/email-inbox-classic-view/src/index.ts new file mode 100644 index 00000000..c08f8ad7 --- /dev/null +++ b/packages/email-inbox-classic-view/src/index.ts @@ -0,0 +1,5 @@ +export { TopNavigation } from './TopNavigation'; +export { CategoryButtons } from './CategoryButtons'; +export { SearchFilterBar } from './SearchFilterBar'; +export { EmailListItem } from './EmailListItem'; +export { ClassicInboxView } from './ClassicInboxView'; diff --git a/packages/email-inbox-classic-view/src/types/index.ts b/packages/email-inbox-classic-view/src/types/index.ts new file mode 100644 index 00000000..6bb1405d --- /dev/null +++ b/packages/email-inbox-classic-view/src/types/index.ts @@ -0,0 +1,60 @@ +// Types for the Classic Inbox View + +export type InboxCategory = 'inbox' | 'sent' | 'drafts' | 'all_mail'; + +export type MailLabel = 'primary' | 'promotions' | 'updates'; + +export interface Email { + id: string; + from: { + name: string; + email: string; + avatar?: string; + }; + subject: string; + preview: string; + timestamp: Date; + isOpened: boolean; + isStarred: boolean; + hasAttachments: boolean; + category: MailLabel; + labels: string[]; +} + +export interface InboxState { + activeCategory: InboxCategory; + activeLabel: MailLabel; + emails: Email[]; + searchQuery: string; + isLoading: boolean; + isRefreshing: boolean; + error: string | null; +} + +export interface NavButton { + id: string; + label: string; + icon: string; + route: string; + isActive: boolean; +} + +export const NAV_BUTTONS: NavButton[] = [ + { id: 'flow', label: 'Flow', icon: 'flow', route: '/flow', isActive: false }, + { id: 'dashboard', label: 'Dashboard', icon: 'dashboard', route: '/dashboard', isActive: false }, + { id: 'classic', label: 'Classic', icon: 'classic', route: '/inbox', isActive: true }, + { id: 'compose', label: 'Compose', icon: 'compose', route: '/compose', isActive: false }, +]; + +export const CATEGORY_ICONS: Record = { + inbox: 'inbox', + sent: 'sent', + drafts: 'drafts', + all_mail: 'all_mail', +}; + +export const LABEL_ICONS: Record = { + primary: 'primary', + promotions: 'promotions', + updates: 'updates', +}; diff --git a/packages/email-inbox-classic-view/storybook/Inbox.stories.tsx b/packages/email-inbox-classic-view/storybook/Inbox.stories.tsx new file mode 100644 index 00000000..fd3c33dd --- /dev/null +++ b/packages/email-inbox-classic-view/storybook/Inbox.stories.tsx @@ -0,0 +1,135 @@ +import React from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import { TopNavigation } from '../src/components/TopNavigation'; +import { CategoryButtons } from '../src/components/CategoryButtons'; +import { SearchFilterBar } from '../src/components/SearchFilterBar'; +import { EmailListItem } from '../src/components/EmailListItem'; +import { ClassicInboxView } from '../src/components/ClassicInboxView'; +import { Email, MailLabel } from '../src/types'; + +// Sample emails for stories +const sampleEmails: Email[] = [ + { + id: '1', + from: { name: 'Alice Johnson', email: 'alice@example.com' }, + subject: 'Project Update - Q2 Review', + preview: 'Hey, here is the latest update on the Q2 project review...', + timestamp: new Date(Date.now() - 1000 * 60 * 30), + isOpened: false, + isStarred: true, + hasAttachments: true, + category: 'primary', + labels: ['primary'], + }, + { + id: '2', + from: { name: 'Bob Smith', email: 'bob@example.com' }, + subject: 'Weekly Standup Notes', + preview: 'Meeting notes from this week's standup...', + timestamp: new Date(Date.now() - 1000 * 60 * 60 * 2), + isOpened: true, + isStarred: false, + hasAttachments: false, + category: 'primary', + labels: ['primary'], + }, + { + id: '3', + from: { name: 'Carol Davis', email: 'carol@example.com' }, + subject: 'Special Offer - 20% Discount', + preview: 'Exclusive discount just for you...', + timestamp: new Date(Date.now() - 1000 * 60 * 60 * 5), + isOpened: false, + isStarred: false, + hasAttachments: false, + category: 'promotions', + labels: ['promotions'], + }, + { + id: '4', + from: { name: 'GitHub', email: 'noreply@github.com' }, + subject: '[warpspeed] New pull request #67', + preview: 'A new pull request has been opened...', + timestamp: new Date(Date.now() - 1000 * 60 * 60 * 24), + isOpened: true, + isStarred: false, + hasAttachments: false, + category: 'updates', + labels: ['updates'], + }, +]; + +// --- TopNavigation Story --- +const meta1: Meta = { + title: 'Inbox/TopNavigation', + component: TopNavigation, + parameters: { layout: 'fullscreen' }, +}; +export default meta1; +type Story1 = StoryObj; +export const DefaultNav: Story1 = { + args: { + activeNav: 'classic', + accountName: 'user@warpspeed.com', + onNavChange: (id) => console.log('Nav:', id), + }, +}; + +// --- CategoryButtons Story --- +const meta2: Meta = { + title: 'Inbox/CategoryButtons', + component: CategoryButtons, +}; +export const DefaultCategories: StoryObj = { + args: { + activeLabel: 'primary', + counts: { primary: 12, promotions: 5, updates: 8 }, + }, +}; + +// --- SearchFilterBar Story --- +const meta3: Meta = { + title: 'Inbox/SearchFilterBar', + component: SearchFilterBar, +}; +export const DefaultSearch: StoryObj = { + args: { + searchQuery: '', + onSearchChange: (q) => console.log('Search:', q), + onFilterPress: () => console.log('Filter'), + }, +}; + +// --- EmailListItem Story --- +const meta4: Meta = { + title: 'Inbox/EmailListItem', + component: EmailListItem, +}; +export const UnreadEmail: StoryObj = { + args: { email: sampleEmails[0], onPress: (e) => console.log('Press:', e.id) }, +}; +export const ReadEmail: StoryObj = { + args: { email: sampleEmails[1], onPress: (e) => console.log('Press:', e.id) }, +}; + +// --- ClassicInboxView Story --- +const meta5: Meta = { + title: 'Inbox/ClassicInboxView', + component: ClassicInboxView, + parameters: { layout: 'fullscreen' }, +}; +export const FullInbox: StoryObj = { + args: { + emails: sampleEmails, + activeCategory: 'inbox', + activeLabel: 'primary', + searchQuery: '', + isRefreshing: false, + onNavChange: (id) => console.log('Nav:', id), + onLabelChange: (label) => console.log('Label:', label), + onSearchChange: (q) => console.log('Search:', q), + onFilterPress: () => console.log('Filter'), + onEmailPress: (e) => console.log('Email:', e.id), + onRefresh: () => console.log('Refresh'), + }, +}; diff --git a/packages/email-inbox-classic-view/tsconfig.json b/packages/email-inbox-classic-view/tsconfig.json new file mode 100644 index 00000000..ca1eb6f0 --- /dev/null +++ b/packages/email-inbox-classic-view/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "lib": ["ES2022"], + "jsx": "react-jsx", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +}